
/* GCSx
** COMPILE.CPP
**
** Bytecode compilation (to pre-link state)
*/

/*****************************************************************************
** Copyright (C) 2003-2006 Janson
**
** This program is free software; you can redistribute it and/or modify
** it under the terms of the GNU General Public License as published by
** the Free Software Foundation; either version 2 of the License, or
** (at your option) any later version.
** 
** This program is distributed in the hope that it will be useful,
** but WITHOUT ANY WARRANTY; without even the implied warranty of
** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
** GNU General Public License for more details.
**
** You should have received a copy of the GNU General Public License
** along with this program; if not, write to the Free Software
** Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111, USA.
*****************************************************************************/

#include "all.h"

// Code has been ear-marked for places that must be revisted if certain
// features are later added; search for the following:
//   OPERATOR++ (refers to prefix or postfix -- or ++)
//   OPERATOR+= (refers to the entire family of similar assignment operators)

// @TODO: all compiler and tokenizer asserts need throw_Compile and need
//   catch blocks if memory resources need to be freed (such as Operands)

// @TODO: sub-expression memoization

// @TODO:
//  warning if a function definitively never returns and isn't main ('end' doesn't count?)
//  some optimizations:
//   value left over after an expression (e.g. a bare function) doesn't HAVE to be DISCARD'd yet (can accumulate, but watch out for labels/etc)
//   DISCARD followed by PUSH could sometimes be a STORE
//   POP followed by equivalent PUSH could be a STORE
//   RETurning local[0] doesn't need it to be PUSHed first (just return it and reduce # of entries discarded)
//     In fact, returning local[x] could be done as DISCARD, RET instead of PUSH, RET (would be faster)
//   JUMP, IFTRUE, IFFALSE, SUBR to a JUMP, RET, or STOP is redundant (SUBR to RET shouldn't happen)
//   IFTRUE, IFFALSE followed by a JUMP should be condensed (flipping the condition)
//   IFTRUE, IFFALSE, JUMP to the next opcode should be removed
//   (remove NOOPs if ever used)
//   PUSH of a literal followed by later converting that literal (ex: float a = 5 / b does this)
//   DISCARD followed by STOP is unneeded
//   PUSH followed by DISCARD should be removed

// defining members for built-in object types
struct ObjectMember {
    int objtype;
    const char* name;
    int index;
    DataType membertype;
    int readonly;
};
static ObjectMember memberTable[] = {
    { SUB_SPRITE, "x",          Sprite::PROP_X,         { DATA_INT, 0, 0 }, 0 },
    { SUB_SPRITE, "y",          Sprite::PROP_Y,         { DATA_INT, 0, 0 }, 0 },
    { SUB_SPRITE, "originx",    Sprite::PROP_ORIGIN_X,  { DATA_INT, 0, 0 }, 0 },
    { SUB_SPRITE, "originy",    Sprite::PROP_ORIGIN_Y,  { DATA_INT, 0, 0 }, 0 },
    { SUB_SPRITE, "priority",   Sprite::PROP_PRIORITY,  { DATA_INT, 0, 0 }, 1 },
    { SUB_ENTITY, "name",       Entity::PROP_NAME,      { DATA_STR, 0, 0 }, 1 },
    { SUB_ENTITY, "sprite",     Entity::PROP_SPRITE,    { DATA_OBJECT, SUB_SPRITE, 0 }, 0 },
    { SUB_ENTITY, "priority",   Entity::PROP_PRIORITY,  { DATA_INT, 0, 0 }, 1 },
    { 0, NULL, 0, OM_NONE, 0 }
};

Compiler::Compiler(const list<string>* src, const World* srcWorld) : stringMap(), main(), localInit(), functions(), strings() { start_func
    source = src;
    world = srcWorld;
    t = NULL;
    func = NULL;
    vars = NULL;
    labels = NULL;

    errorCount = 0;
    warningCount = 0;
    
    inFunctionScope = 0;
    
    main.stackDepth = 0;
    localInit.stackDepth = 0;
    functions.stackDepth = 0;
    strings.stackDepth = 0;
}

Compiler::~Compiler() { start_func
    delete t;
    destroyVariableMap(vars);
    delete vars;
    destroyLabelMap(labels);
    delete labels;
}

int Compiler::parseSymbols(FunctionMap* funcMap) { start_func
    assert(funcMap);
    
    t = new Tokenizer(source);
    func = funcMap;
    
    errorCount = 0;

#ifdef COMPILEASSERT    
    try {
#endif
        doParseSymbols();
#ifdef COMPILEASSERT
    }
    catch (CompileException& e) {
        destroyFunctionMap(funcMap);
        errorCount = 1;
    }
#endif
    
    func = NULL;
    delete t;
    t = NULL;
    
    return errorCount;
}
    
int Compiler::compile(Uint32** bytecode, FunctionMap* funcMap, list<LinkEntry>* links, int scriptId) { start_func
    assert(bytecode);
    assert(funcMap);
    assert(links);
    assert(scriptId >= SUB_ENTITY_FIRST);
    
    t = new Tokenizer(source);
    func = funcMap;
    vars = new VariableMap;
    labels = new LabelMap;
    scriptType = scriptId;
    
#ifdef COMPILEASSERT    
    try {
#endif
        doCompile(bytecode, links);
#ifdef COMPILEASSERT    
    }
    catch (CompileException& e) {
        t->outputError(e.details);
    }
#endif

    errorCount = t->numErrors();
    warningCount = t->numWarnings();

    destroyVariableMap(vars);
    delete vars;
    destroyLabelMap(labels);
    delete labels;
    labels = NULL;
    vars = NULL;
    func = NULL;
    delete t;
    t = NULL;
    
    return errorCount;
}

void Compiler::doParseSymbols() { start_func
    assert(func);
    assert(t);

    int type;
    string token;
    
    // Ensure error output is off    
    t->silentErrors();
    
    // Scan for functions, format- (parens much match if present)
    // [datatype] identifier [(] [datatype identifier [, datatype identifier ...]] [)] {
        
    // Also scan for scopes, format-
    // state identifier {
        
    // Will also handle certain # configuration commands
        
    // Any other commands or entire blocks are ignored
    
    DataType datatype;
    datatype.baseType = 0;
    int keywState = 0;
    string state;
    string ident;
    while (t->peekToken(type, token)) {
        int doSkip = 0;
        
        if (isDataType(type)) {
            // Note datatype and loop for next token
            datatype = parseDataType();
            if (keywState) doSkip = 1;
        }
        else if (type == TOKEN_IDENTIFIER) {
            ident = token;
            t->skipToken();
            if (keywState) {
                // Next token MUST be brace
                t->peekToken(type, token);
                if (type == TOKEN_OPEN_BRACE) {
                    state = ident + "::";
                    t->skipToken();
                }
                // Anything that doesn't follow the above format is just ignored
                // (main compile will remark on errors)
                keywState = 0;
            }
            else if (datatype.baseType) {
                vector<Parameter> plist;
                
                if (parseParameterList(plist)) {
                    // Next token MUST be brace
                    t->peekToken(type, token);
                    if (type == TOKEN_OPEN_BRACE) {
                        // Build function struct
                        Function newFunc;
                        newFunc.returntype = datatype;
                        newFunc.returntype.flags &= ~(DATA_CONST | DATA_LOCAL | DATA_GLOBAL | DATA_PRIVATE | DATA_PUBLIC);
                        newFunc.offset = -1;
                        newFunc.numparam = plist.size();
                        if (newFunc.numparam) {
                            // Compile list of parameter types
                            DataType* where = newFunc.parameters = new DataType[newFunc.numparam];
                            for (vector<Parameter>::iterator pos = plist.begin(); pos != plist.end(); ++pos) {
                                *where++ = (*pos).type;
                            }
                        }
                        else newFunc.parameters = NULL;
                        // Build function name
                        ident = state + ident;
                        // Store in map (overwrites previous if found)
                        addFunctionMap(func, ident) = newFunc;
                        // Don't skip token- we need to see { to skip function block
                    }
                }
                // Anything that doesn't follow the above format is just ignored
                // (main compile will remark on errors)
                doSkip = 1;
            }
            else {
                doSkip = 1;
            }
        }
        else if (type == KEYW_STATE) {
            // Loop for next token
            keywState = 1;
            t->skipToken();
            if (datatype.baseType) doSkip = 1;
        }
        else if (type == TOKEN_CLOSE_BRACE) {
            state = blankString;
            t->skipToken();
            datatype.baseType = 0;
            keywState = 0;
        }
        else {
            doSkip = 1;
        }
        
        if (doSkip) {
            // Loop until end of line, skipping over blocks
            int blockLevel = 0;
            while (t->nextToken(type, token)) {
                if (type == TOKEN_OPEN_BRACE) ++blockLevel;
                if (type == TOKEN_CLOSE_BRACE) --blockLevel;
                if (((type == TOKEN_ENDLINE) || (type ==  TOKEN_CLOSE_BRACE)) &&
                    (blockLevel == 0)) break;
            }
            datatype.baseType = 0;
            keywState = 0;
        }
    }
}

int Compiler::isDataType(int tokenType) { start_func
    if ((tokenType == KEYW_ARRAY) || (tokenType == KEYW_HASH) ||
        (tokenType == KEYW_INT) || (tokenType == KEYW_STR) ||
        (tokenType == KEYW_FLOAT) || (tokenType == KEYW_VAR) ||
        (tokenType == KEYW_GLOBAL) || (tokenType == KEYW_LOCAL) ||
        (tokenType == KEYW_CONST) || (tokenType == KEYW_VOID) ||
        (tokenType == KEYW_PRIVATE) || (tokenType == KEYW_PUBLIC) ||
        (tokenType == KEYW_OBJ_ENTITY) || (tokenType == KEYW_OBJ_SPRITE) ||
        (tokenType == KEYW_OBJ_SCENE) || (tokenType == TOKEN_STRINGTYPE))
        return 1;
    return 0;
}

DataType Compiler::parseDataType() { start_func
    DataType datatype;
    datatype.baseType = 0;
    datatype.flags = 0;
    datatype.subType = 0;
    int type;
    string token;
    while ((t->peekToken(type, token)) && (isDataType(type))) {
        t->skipToken();

        int base = 0, sub = 0, varConflict = 0, objConflict = 0;
        int flag = 0, flagConflict = 0;
        switch (type) {
            case KEYW_ARRAY:
                flag = DATA_ARRAY;
                flagConflict = DATA_HASH | DATA_CONST;
                break;
            case KEYW_HASH:
                flag = DATA_HASH;
                flagConflict = DATA_ARRAY | DATA_CONST;
                break;
            case KEYW_CONST:
                flag = DATA_CONST;
                flagConflict = DATA_HASH | DATA_CONST;
                varConflict = 1;
                objConflict = 1;
                break;
            case KEYW_VOID:
                base = DATA_VOID;
                flagConflict = DATA_HASH | DATA_ARRAY | DATA_CONST | DATA_LOCAL | DATA_GLOBAL;
                break;
            case KEYW_VAR:
                base = DATA_VAR;
                flagConflict = DATA_CONST;
                break;
            case KEYW_STR:
                base = DATA_STR;
                break;
            case KEYW_FLOAT:
                base = DATA_FLOAT;
                break;
            case KEYW_INT:
                base = DATA_INT;
                break;
            case KEYW_OBJ_ENTITY:
                // obj_entity doesn't conflict with script type
                if ((datatype.baseType == DATA_ENTITY) && (datatype.subType))
                    datatype.baseType = 0;
                base = DATA_ENTITY;
                sub = 0;
                flagConflict = DATA_CONST;
                break;
            case KEYW_OBJ_SPRITE:
                base = DATA_OBJECT;
                sub = SUB_SPRITE;
                flagConflict = DATA_CONST;
                break;
            case KEYW_OBJ_SCENE:
                base = DATA_OBJECT;
                sub = SUB_SCENE;
                flagConflict = DATA_CONST;
                break;
            case KEYW_LOCAL:
                flag = DATA_LOCAL;
                flagConflict = DATA_GLOBAL;
                break;
            case KEYW_GLOBAL:
                flag = DATA_GLOBAL;
                flagConflict = DATA_LOCAL | DATA_PUBLIC | DATA_PRIVATE;
                break;
            case KEYW_PRIVATE:
                flag = DATA_PRIVATE;
                flagConflict = DATA_PUBLIC | DATA_GLOBAL;
                break;
            case KEYW_PUBLIC:
                flag = DATA_PUBLIC;
                flagConflict = DATA_PRIVATE | DATA_GLOBAL;
                break;
                
            case TOKEN_STRINGTYPE: {
                // script type doesn't conflict with obj_entity
                if ((datatype.baseType == DATA_ENTITY) && (datatype.subType == 0))
                    datatype.baseType = 0;
                
                Script* found = world->findScriptCode(token);
                if (found) {
                    sub = found->getId();
                }
                else {
                    t->outputError("Unknown script type '%s' (no script by that name)", token.c_str());
                    // RESOLUTION: fake sub type
                    sub = SUB_ENTITY_FIRST;
                }
                base = DATA_ENTITY;
                break;
            }
        }
        
        if ((varConflict) && (datatype.baseType == DATA_VAR) ||
            (objConflict) && (datatype.baseType == DATA_OBJECT || datatype.baseType == DATA_ENTITY)) {
            t->outputError("Conflicting datatype '%s' (two or more given types cannot be used together)", token.c_str());
            // RESOLUTION: ignore conflicting datatype
        }
        else {
            if (base) {
                if (datatype.baseType == base) {
                    t->outputWarning("Duplicated datatype '%s' (datatype is listed twice)", token.c_str());
                    // RESOLUTION: ignore duplicated datatype
                }
                else if (datatype.baseType) {
                    t->outputError("Conflicting datatype '%s' (two or more given types cannot be used together)", token.c_str());
                    // RESOLUTION: ignore conflicting datatype
                }
                else {
                    datatype.baseType = base;
                    datatype.subType = sub;
                }
            }
            if (datatype.flags & flag) {
                t->outputWarning("Duplicated datatype '%s' (datatype is listed twice)", token.c_str());
                // RESOLUTION: ignore duplicated datatype
            }
            else if (datatype.flags & flagConflict) {
                t->outputError("Conflicting datatype '%s' (two or more given types cannot be used together)", token.c_str());
                // RESOLUTION: ignore conflicting datatype
            }
            else {
                datatype.flags |= flag;
            }
        }
    }
    
    if (!datatype.baseType) {
        t->outputError("No base datatype given (must specify 'int', 'str', 'float', etc.)");
        // RESOLUTION: assume int
        datatype.baseType = DATA_INT;
    }
    
    return datatype;
}

int Compiler::parseParameterList(vector<Parameter>& list) { start_func
    int type;
    string token;
    assert(list.empty());

    // Optional paren
    int paren = 0;
    t->peekToken(type, token);
    if (type == TOKEN_OPEN_PAREN) {
        paren = 1;
        t->skipToken();
        t->peekToken(type, token);
    }
    
    // parameters?
    if (isDataType(type)) {
        for (;;) {
            // (already peeked, above, or at end of loop below)
            DataType vartype = parseDataType();
            checkDataType(vartype, 1, 1, 1, 1);
            t->peekToken(type, token);
            if (type != TOKEN_IDENTIFIER) {
                t->outputError("Expected parameter identifier here");
                // RESOLUTION: parameter list unusable
                return 0;
            }

            // Matches a function?
            FunctionMap::iterator flist = func->find(token.c_str());
            if (flist != func->end()) {
                t->outputError("Function '%s' already exists in this script (parameter variables and functions cannot share names)", token.c_str());
                // RESOLUTION: define parameter anyways
            }
        
            // Matches a built-in?
            for (int pos = 0; memberTable[pos].objtype; ++pos) {
                if ((memberTable[pos].objtype == SUB_ENTITY) &&
                    (token == memberTable[pos].name)) {
                    t->outputError("'%s' is a built-in entity member (parameter variables cannot override built-in members)", token.c_str());
                    // RESOLUTION: define parameter anyways
                }
            }

            // All parameters are marked as local
            vartype.flags |= DATA_LOCAL;
            Parameter param = { vartype, token };
            list.push_back(param);
            t->skipToken();
            
            t->peekToken(type, token);
            if (type == TOKEN_COMMA) {
                t->skipToken();
                t->peekToken(type, token);
                if (!isDataType(type)) {
                    t->outputError("Expected parameter here (datatype, then identifier)");
                    // RESOLUTION: assume no more parameters
                    break;
                }
            }
            else break;
        }
    }
    
    // End paren? (already peeked previously)
    if (type == TOKEN_CLOSE_PAREN) {
        t->skipToken();
        if (!paren) {
            t->outputError("Missing open parenthesis to match closing parenthesis");
            // RESOLUTION: pretend paren was there
        }
    }
    else if (paren) {
        t->outputError("Missing closing parenthesis to match open parenthesis");
        // RESOLUTION: pretend paren was there
    }
    
    return (list.empty() && (!paren)) ? -1 : 1;
}

void Compiler::doCompile(Uint32** bytecode, list<LinkEntry>* links) { start_func
    assert(bytecode);
    assert(func);
    assert(t);
    assert(vars);
    assert(labels);
    assert(links);
    
    // First, dealloc any existing bytecode
    if (*bytecode) {
        delete[] *bytecode;
        *bytecode = NULL;
    }
    
    // All areas should already be clear
    compileAssert(main.code.empty());
    compileAssert(main.links.empty());
    compileAssert(main.stackDepth == 0);
    compileAssert(localInit.code.empty());
    compileAssert(localInit.links.empty());
    compileAssert(localInit.stackDepth == 0);
    compileAssert(functions.code.empty());
    compileAssert(functions.links.empty());
    compileAssert(functions.stackDepth == 0);
    compileAssert(strings.code.empty());
    compileAssert(strings.links.empty());
    compileAssert(strings.stackDepth == 0);
    compileAssert(workspaces.empty());
    compileAssert(stringMap.empty());
    
    // Initially adding code to main
    pushWorkspace(&main);

    scope = -1; // lowest scope will be 0
    inFunctionScope = 0;
    Ended junk;
    compileBlock(junk);
    compileAssert(scope == -1);
    compileAssert(inFunctionScope == 0);
    
    // Get sizes
    int sizeI = localInit.code.size();
    int sizeIM = sizeI + main.code.size();
    int sizeIMF = sizeIM + functions.code.size();
    int sizeIMFS = sizeIMF + strings.code.size();
    
    // Final links adjustment-
    //   link local strings/functions
    //   move others to final link table
    // (localInit probably won't have links, but it could happen with future improvements)
    links->clear();
    completeLinkage(links, localInit, 0, sizeIMF, sizeIM);
    completeLinkage(links, main, sizeI, sizeIMF - sizeI, sizeIM - sizeI);
    completeLinkage(links, functions, sizeIM, sizeIMF - sizeIM, 0);
    compileAssert(strings.links.empty());
    
    // Move code back to bytecode
    Uint32* work = new Uint32[2 + sizeIMFS];
    *bytecode = work;
    *work++ = sizeIMFS;
    *work++ = sizeIMF;
    
    vector<Uint32>::iterator pos;
    vector<Uint32>::iterator end = localInit.code.end();
    for (pos = localInit.code.begin(); pos != end; ++pos) *work++ = *pos;

    end = main.code.end();
    for (pos = main.code.begin(); pos != end; ++pos) *work++ = *pos;

    end = functions.code.end();
    for (pos = functions.code.begin(); pos != end; ++pos) *work++ = *pos;

    end = strings.code.end();
    for (pos = strings.code.begin(); pos != end; ++pos) *work++ = *pos;

    // Clean up
    main.code.clear();
    compileAssert(main.links.empty());
    compileAssert(main.stackDepth == 0);
    localInit.code.clear();
    compileAssert(localInit.links.empty());
    localInit.stackDepth = 0;
    functions.code.clear();
    compileAssert(functions.links.empty());
    compileAssert(functions.stackDepth == 0);
    strings.code.clear();
    compileAssert(strings.links.empty());
    compileAssert(strings.stackDepth == 0);
    workspaces.pop_back();
    compileAssert(workspaces.empty());

    // We clear one at a time to ensure we can delete hash keys at the right times
    StringMap::iterator smpos = stringMap.begin();
    while (smpos != stringMap.end()) {
        const char* toDel = (*smpos).first;
        stringMap.erase(smpos);
        delete[] toDel;
        smpos = stringMap.begin();
    }
}

void Compiler::completeLinkage(list<LinkEntry>* links, Codespace& code, int linkOffset, int strOffset, int funcOffset) { start_func
    list<LinkEntry>::iterator pos = code.links.begin();
    list<LinkEntry>::iterator end = code.links.end();
    for (; pos != end; ++pos) {
        if ((*pos).type == LinkEntry::LINK_STRING) {
            code.code[(*pos).offset] += strOffset - (*pos).offset;
        }
        else if ((*pos).type == LinkEntry::LINK_FUNCTION) {
            FunctionMap::iterator flist = func->find((*pos).name.c_str());
            compileAssert(flist != func->end());
            compileAssert((*flist).second.offset >= 0);
            code.code[(*pos).offset] += (*flist).second.offset + funcOffset - (*pos).offset;
        }
        else {
            compileAssert((*pos).type != LinkEntry::LINK_LOOP_CONTINUE);
            compileAssert((*pos).type != LinkEntry::LINK_LOOP_END);
            (*pos).offset += linkOffset;
            links->push_back(*pos);
        }
    }
    code.links.clear();
}

void Compiler::compileBlock(Ended& ended, const vector<Parameter>* parameters, DataType* returnType) { start_func
    int type;
    string token;
    int braceBlock = 0;
    // Skip remainder of command (until end line or open brace)
    int skipCmd = 0;
    int exitBlock = 0;
    int detectEndOfCommand;
    // stackDepth upon entering this code block, so we can discard all NEW vars
    int entryStackDepth = ws->stackDepth;
    int paramStackDepth = entryStackDepth;
    Label* loopLabel = NULL;
    string loopLabelName;
    int justDidLoopLabel = 0;
    
    ++scope;
    
    enum {
        // least-to-most restrictive-
        //  combining two values (if/else) results in higher scope, lower 'how' if scope tied
        ENDED_NONE = 0,
        ENDED_CONTINUE = 1, // continue- full stop until it's loop boundary
        ENDED_BREAK = 2, // break- full stop until it's loop boundary, also skips do{} ending condition
        ENDED_RESTART = 3, // restart- full stop until it's loop boundary, becomes full stop if a do{} loop
        ENDED_RETURN = 4, // return/end/infinite loop- full stop (scope set to 0 to always 'lose')
    };
    // If set, we've returned/stopped, any remaining code is redundant and might get discarded
    ended.how = ENDED_NONE;
    // For loops, scope of inner loop block that it applies to
    ended.scope = scope + 2; // Default to 'highest' scope, so this always 'wins'
    // Scope of farthest-out potentially-infinite-loop to break
    ended.breakInfinite = scope + 2; // Default to breaking nothing

    // Define parameters and function start?
    if (parameters) {
        for (vector<Parameter>::const_iterator pos = parameters->begin(); pos != parameters->end(); ++pos) {
            defineVariable((*pos).type, (*pos).name, 1);
        }
        // reserve spot for code return pointer
        ++ws->stackDepth;
        paramStackDepth = ws->stackDepth;
        
        inFunctionScope = scope;
        funcEntryStackDepth = entryStackDepth;
        funcParamStackDepth = paramStackDepth;
    }
    
    if (scope == 0) {
        // (local variable initialization will go right before our workspace)
        postCmd(OP_INIT);
    }
    else {
        // Beyond top scope, a block can be a single cmd or { block }
        // Discard any stray endline
        t->peekToken(type, token);
        if (type == TOKEN_ENDLINE) {
            t->skipToken();
            t->peekToken(type, token);
        }
        // Does our block start with a brace?
        if (type == TOKEN_OPEN_BRACE) {
            t->skipToken();
            braceBlock = 1;
        }
    }
    
    while (t->peekToken(type, token)) {
        detectEndOfCommand = 0;
        
        // Loop label only persists for one token
        if (justDidLoopLabel)
            justDidLoopLabel = 0;
        else {
            loopLabel = NULL;
            loopLabelName = blankString;
        }
        
        // Skipping stuff?
        if ((skipCmd) && (type != TOKEN_OPEN_BRACE)) {
            if (type == TOKEN_ENDLINE) skipCmd = 0;
            t->skipToken();
            // that was our one command, if not a brace block
            if ((!braceBlock) && (scope)) break;
            // next command
            continue;
        }
        skipCmd = 0;
        
        // Ended code? Something other than a closing brace?
        if ((ended.how) && (type != TOKEN_CLOSE_BRACE)) {
            t->outputWarning("Unreachable code (previous code will prevent this code from ever being executed)");
            // RESOLUTION: skip everything until our closing brace, but leave that
            int blockLevel = 0;
            while (t->peekToken(type, token)) {
                if (type == TOKEN_OPEN_BRACE) ++blockLevel;
                if (type == TOKEN_CLOSE_BRACE) --blockLevel;
                if (blockLevel < 0) break;
                t->skipToken();
            }
            continue;
        }
        
        // As a general rule, nothing is left on the stack between full commands
        // except for new local variables. Temporaries that must stay on the stack
        // must either be right before starting a new block, or be assigned
        // an internal temporary local variable; this may change if we determine
        // leaving temporaries on the stack can be safe in some situations

        // Detect datatypes first, outside of switch
        if (isDataType(type)) {
            // Datatype for definition
            DataType datatype = parseDataType();
            
            // Next MUST be identifier
            t->peekToken(type, token);
            if (type != TOKEN_IDENTIFIER) {
                t->outputError("Expected identifier after datatype (appears to be a function or variable definition)");
                // RESOLUTION: ignore rest of command
                skipCmd = 1;
            }
            else {
                // Identifier
                string ident = token;
                t->skipToken();
                
                // Parameters?
                vector<Parameter> plist;
                int paramResult = parseParameterList(plist);
                
                // Function definition, variable definition, or error?
                t->peekToken(type, token);
                if ((paramResult) && (type == TOKEN_OPEN_BRACE)) {
                    // Error-check datatype
                    checkDataType(datatype, 1, 1, 0, 0);

                    if (scope) {
                        t->outputError("Functions must be defined outside all other code blocks");
                        // RESOLUTION: open brace will just be treated as a scope block now
                    }
                    else {
                        // PARSE: FUNCTION DEFINITION
                        // Starts at open-brace (denotes block)
                        // Note position in list
                        FunctionMap::iterator flist = func->find(ident.c_str());
                        // (if not found, we shouldn't probably be here, but
                        //  might happen in some situations with symbol preparser)
                        if (flist != func->end()) {
                            if ((*flist).second.offset >= 0)
                                t->outputError("Function '%s' defined twice (you can only create one function with a given name)", ident.c_str());
                                // RESOLUTION: code gets added, but never referenced
                            else
                                // Store offset in function list
                                (*flist).second.offset = functions.code.size();
                        }

                        VariableMap::iterator vlist = vars->find(ident.c_str());
                        if (vlist != vars->end()) {
                            t->outputError("Variable '%s' already exists in this script (variables and functions cannot share names)", ident.c_str());
                            // RESOLUTION: define function anyways
                        }
                        
                        for (int pos = 0; memberTable[pos].objtype; ++pos) {
                            if ((memberTable[pos].objtype == SUB_ENTITY) &&
                                (ident == memberTable[pos].name)) {
                                t->outputError("'%s' is a built-in entity member (functions cannot override built-in members)", ident.c_str());
                                // RESOLUTION: define function anyways
                            }
                        }

                        // Parse function block
                        compileAssert(ws == &main);
                        pushWorkspace(&functions);
                        Ended junk;
                        compileBlock(junk, &plist, &datatype);
                        popWorkspace();
                        compileAssert(functions.stackDepth == 0);
                    }
                }
                else if ((paramResult < 0) && ((type == TOKEN_ENDLINE) || (type == OPER_ASSIGN))) {
                    // Error-check datatype
                    checkDataType(datatype, 0, 0, 0, 1);

                    // PARSE: VARIABLE DEFINITION
                    const Variable& v = defineVariable(datatype, ident);
                    if (type == OPER_ASSIGN) {
                        // PARSE: VARIABLE ASSIGNMENT
                        // (discard =)
                        t->skipToken();

                        Operand var = variableToOperand(v);
                        parseAssignment(var, v.datatype);
                        
                        // Should be end of command now
                        t->peekToken(type, token);
                    }
                    
                    detectEndOfCommand = 1;
                }
                else {
                    // Errors
                    if (paramResult > 0)
                        t->outputError("Expected open brace (appears to be a function definition)");
                    else if (paramResult < 0)
                        t->outputError("Expected open brace, assignment, or end-of-command (appears to be a function or variable definition)");
                    // (if 0, parseParameterList already output an error)
                    // RESOLUTION: ignore rest of command
                    skipCmd = 1;
                }
            }
        }
        else {
            switch (type) {
                // These types can all start an expression, although currently
                // expressions starting with these are gauranteed to be 'wrong'
                // in that at least a portion will have no side-effects and should
                // produce a warning. Future operators may make these more useful
                // so they are here now.
                case TOKEN_INTEGER:
                case TOKEN_HEX:
                case TOKEN_DECIMAL:
                case KEYW_TRUE:
                case KEYW_FALSE:
                case KEYW_QUERY:
                case OPER_PLUS:
                case OPER_MINUS:
                case OPER_B_NOT:
                case OPER_L_NOT:
                case KEYW_NOTHING:
                // These tokens can actually start a useful 'standalone' expression.
                case KEYW_ALL:
                case KEYW_ALL_OTHER:
                case KEYW_THIS:
                case KEYW_SOURCE:
                case KEYW_DEFAULT:
                case TOKEN_STRING:
                case TOKEN_OPEN_PAREN:
                case TOKEN_IDENTIFIER: {
                    // Label?
                    if (type == TOKEN_IDENTIFIER) {
                        t->bookmarkStore(1);
                        t->skipToken();
                        string nextToken;
                        if ((t->nextToken(type, nextToken)) && (type == TOKEN_LABEL)) {
                            // Process label token
                            t->bookmarkCancel(1);

                            Label newLabel;
                            newLabel.scope = scope;
                            newLabel.offset = ws->code.size();
                            newLabel.isLoop = 0;
                            newLabel.stackDepth = ws->stackDepth;

                            // Store label
                            list<Label>& found = addLabelMap(labels, token);
                            found.push_back(newLabel);
                            // (in case a loop is next)
                            loopLabel = &found.back();
                            loopLabelName = token;
                            justDidLoopLabel = 1;

                            // Use up end-of-command IF present (doesn't count)
                            t->peekToken(type, token);
                            if (type == TOKEN_ENDLINE) t->skipToken();
                            break;
                        }
                        else {
                            t->bookmarkReturn(1);
                            t->bookmarkCancel(1);
                        }
                    }
                    
                    // Variable assignment, function call, scope, message send, etc.
                    // All of these are handled via expressions
                    Operand o = parseExpression(0, 0);
                    if (!O_IS_SIDEFX(o) && !O_IS_VOID(o))
                        t->outputWarning("Part or all of expression has no effect (no side effects and result is not used)");
                    destroyOperand(o);
                    
                    // Should be end of command now
                    t->peekToken(type, token);
                    detectEndOfCommand = 1;
                    break;
                }

                case TOKEN_OPEN_BRACE: {
                    // Build a sub-block of code starting at brace
                    compileBlock(ended, NULL, returnType);
                    break;
                }

                case TOKEN_CLOSE_BRACE: {
                    t->skipToken();
                    
                    if (scope == 0) {
                        t->outputError("Unexpected close brace (does not match an open brace)");
                        // RESOLUTION: just skip the brace
                    }
                    else {
                        // Done with block
                        exitBlock = 1;
                    }
                    break;
                }
                
                case KEYW_RETURN: {
                    t->skipToken();
                    
                    // Returning anything?
                    Operand o;
                    createEmpty(o);
                    t->peekToken(type, token);
                    if ((type != TOKEN_ENDLINE) && (type != TOKEN_OPEN_BRACE)) {
                        o = parseExpression();
                        t->peekToken(type, token);
                    }
                    
                    processReturn(o, returnType);
                    ended.how = ENDED_RETURN;
                    ended.scope = 0;
                    
                    detectEndOfCommand = 1;
                    break;
                }
                
                case KEYW_END: {
                    t->skipToken();
                    
                    // @TODO: don't show warning if main was used
                    if (inFunctionScope)
                        t->outputWarning("'end' inside of a function halts any suspended functions as well- did you mean 'return'?");
                    
                    postCmd(OP_STOP);
                    ended.how = ENDED_RETURN;
                    ended.scope = 0;

                    t->peekToken(type, token);
                    detectEndOfCommand = 1;
                    break;
                }

                case KEYW_IF: {
                    t->skipToken();
                    
                    // If-condition
                    Operand o = parseExpression(2);
                    if (!o.mode) {
                        skipCmd = 1;
                        break;
                    }
                    
                    // If cmd
                    int alwaysJumps, neverJumps;
                    int offset = postCmdIf(0, 0, o, alwaysJumps, neverJumps);

                    // Build a sub-block of code starting here
                    int codeStart = ws->code.size();
                    Ended endedIf;
                    compileBlock(endedIf, NULL, returnType);

                    // Adjust offset to skip new block
                    if (offset >= 0) ws->code[offset] += ws->code.size() - codeStart;
                    
                    // else clause? (will automatically chain additional ifs)
                    t->peekToken(type, token);
                    if (type == KEYW_ELSE) {
                        t->skipToken();
                        
                        // Add to 'if' block a jump to skip past 'else' block IF didn't end
                        if (endedIf.how)
                            offset = -1;
                        else {
                            // original 'if' jump needs to go 2 spaces further
                            if (offset >= 0) ws->code[offset] += 2;
                            // Offset of new 'if' jump (at end of 'if')
                            offset = ws->code.size() + 1; // (remember where operand is)
                            // By default, skips to next opcode
                            postCmdI(OP_JUMP, 0);
                        }

                        // 'else' sub-block of code starting here
                        int codeStart = ws->code.size();
                        Ended endedElse;
                        compileBlock(endedElse, NULL, returnType);

                        // Adjust jump at end of 'if' to skip this block
                        if (offset >= 0) ws->code[offset] += ws->code.size() - codeStart;
                        
                        // If both if and else clauses ended, we ended
                        // Take highest scope
                        if (endedElse.scope > endedIf.scope) {
                            ended.how = endedElse.how;
                            ended.scope = endedElse.scope;
                        }
                        else if (endedIf.scope > endedElse.scope) {
                            ended.how = endedIf.how;
                            ended.scope = endedIf.scope;
                        }
                        else {
                            // Scope equal, take smallest 'how'
                            ended.how = min(endedIf.how, endedElse.how);
                            ended.scope = endedIf.scope; // (same as Else)
                        }
                        ended.breakInfinite = min(endedIf.breakInfinite, endedElse.breakInfinite);
                    }
                    
                    // @TODO: we could, if we know if was always-true or always-false,
                    // fill endedHow/Scope more aggressively
                    // @TODO: discard code entirely if always-true or always-false?
                    
                    break;
                }
                
                case KEYW_ELSE: {
                    t->outputError("'else' can only appear after an 'if' command or block (did you forget braces around your 'if' blocks?)");
                    // RESOLUTION: ignore rest of command
                    skipCmd = 1;
                    break;
                }
                    
                case KEYW_DO:
                case KEYW_UNTIL:
                case KEYW_WHILE: {
                    t->skipToken();
                    
                    int loopOffset = ws->code.size();
                    int continuePos = loopOffset;
                    int condOffset = -1;
                    int infiniteLoop = 0; // <= 0 means no, here (-1 means a break gaurantees no infinite)
                    int neverLoops = 0;
                    int skipJumpBack = 0;

                    // Label marking loop- set scope to sub-block's scope so it
                    // conveniently deletes it for us; use existing label if any
                    Label newLabel;
                    Label* myLabel = loopLabel ? loopLabel : &newLabel;
                    myLabel->scope = scope + 1;
                    myLabel->offset = loopOffset; // For 'restart'; 'continue'/'break' linked later
                    myLabel->isLoop = 1;
                    myLabel->stackDepth = ws->stackDepth;

                    // Pre-condition?
                    if (type != KEYW_DO) {
                        // Loop condition
                        Operand o = parseExpression(2);
                        if (!o.mode) {
                            skipCmd = 1;
                            break;
                        }
                        
                        // Loop 'if'
                        int alwaysJumps, neverJumps;
                        condOffset = postCmdIf(type == KEYW_WHILE ? 0 : 1, 0, o, alwaysJumps, neverJumps);
                        // (must have same stackdepth now)
                        compileAssert(myLabel->stackDepth == ws->stackDepth);
                        // ('restart' position is now here)
                        myLabel->offset = ws->code.size();
                        
                        // Special cases
                        if (alwaysJumps) {
                            neverLoops = 1;
                            t->outputWarning("Code within loop will never execute (loop condition is false and is checked before loop)");
                            // @TODO: discard code entirely?
                        }
                        else if (neverJumps)
                            infiniteLoop = 1;
                    }
                    
                    // Store label if new
                    if (!loopLabel) {
                        list<Label>& found = addLabelMap(labels, loopLabelName);
                        found.push_back(newLabel);
                    }
    
                    // Build a sub-block of code starting here
                    int codeStart = ws->code.size();
                    compileBlock(ended, NULL, returnType);
                    
                    // Handle ended.how now
                    if (ended.how == ENDED_RETURN) {
                        // Return- a 'do' loop or infinite loop- prevails
                        // otherwise- discarded
                        if ((type != KEYW_DO) && (!infiniteLoop)) {
                            ended.how = ENDED_NONE;
                            ended.scope = scope + 2;
                        }
                    }
                    else if (ended.scope > scope) {
                        // Relates to our scope
                        // Restart- a 'do' loop- becomes _RETURN (set later) as it's infinite now
                        if ((ended.how == ENDED_RESTART) && (type == KEYW_DO))
                            infiniteLoop = 1;
                        else if (ended.how > ENDED_NONE) {
                            // User handled all loop control for us (doesn't apply to DO loops)
                            skipJumpBack = 1;
                            
                            // Break- a 'do' loop- final condition is useless,
                            // but code afterwards is executed as normal
                            if ((ended.how == ENDED_BREAK) && (type == KEYW_DO)) {
                                t->outputWarning("Loop condition will never get checked (loop always exits before reaching condition at end)");
                                // @TODO: discard code for loop condition?
                            }
                            ended.how = ENDED_NONE;
                            ended.scope = scope + 2;
                        }
                    }
                    if (ended.breakInfinite <= scope) {
                        // Breaks thru infinite loops
                        infiniteLoop = -1;
                        // All types continue downward to proper scope
                    }

                    // (must have same stackdepth now)
                    compileAssert(myLabel->stackDepth == ws->stackDepth);
                    
                    // Loop expression still needed? (do loop)
                    if (type == KEYW_DO) {
                        if (ended.how) {
                            t->outputWarning("Loop condition will never get checked (loop always repeats or exits before reaching condition at end)");
                            // @TODO: discard code for loop condition?
                        }

                        t->peekToken(type, token);
                        if ((type != KEYW_WHILE) && (type != KEYW_UNTIL)) {
                            t->outputError("Unexpected token '%s'- expected 'while' or 'until'", token.c_str());
                            // RESOLUTION: treat as new command; block is now just a scope-block that may have used loop controls
                            ended.how = ENDED_NONE;
                            ended.scope = scope + 2;
                            break;
                        }

                        // Loop condition
                        continuePos = ws->code.size();
                        t->skipToken();
                        Operand o = parseExpression(2);
                        if (!o.mode) {
                            skipCmd = 1;
                            break;
                        }

                        // Loop 'if'
                        int alwaysJumps, neverJumps;
                        postCmdIf(type == KEYW_WHILE ? 1 : 0, loopOffset - ws->code.size(), o, alwaysJumps, neverJumps);

                        if ((alwaysJumps) && (infiniteLoop == 0))
                            infiniteLoop = 1;
                        
                        // (must have same stackdepth now)
                        compileAssert(myLabel->stackDepth == ws->stackDepth);

                        t->peekToken(type, token);
                        detectEndOfCommand = 1;
                    }
                    else {
                        // Unconditional loop, back to expression, if loop wasn't aborted/looped already
                        // (-2 to account for opcode we're about to use)
                        if (!skipJumpBack)
                            postCmdI(OP_JUMP, loopOffset - ws->code.size() - 2);
                        
                        // Adjust loop jump to skip this block and jump cmd
                        if (condOffset >= 0) ws->code[condOffset] += ws->code.size() - codeStart;
                    }

                    int endPos = ws->code.size();

                    if (infiniteLoop > 0) {
                        // No warnings, they will be given elsewhere if needed (always-true condition)
                        // as an infinite-loop containing an actual return cmd is reasonable
                        ended.how = ENDED_RETURN;
                        ended.scope = 0;
                    }
                    
                    // Finally, handle any links from within loop
                    list<LinkEntry>::iterator end = ws->links.end();
                    list<LinkEntry>::iterator edit;
                    for (list<LinkEntry>::iterator pos = ws->links.begin(); pos != end; ) {
                        edit = pos;
                        ++pos;
                        if (((*edit).type == LinkEntry::LINK_LOOP_CONTINUE) ||
                            ((*edit).type == LinkEntry::LINK_LOOP_END)) {
                            if (((*edit).name == blankString) || ((*edit).name == loopLabelName)) {
                                ws->code[(*edit).offset] +=
                                    (((*edit).type == LinkEntry::LINK_LOOP_CONTINUE) ? continuePos : endPos)
                                    - (*edit).offset;
                                ws->links.erase(edit);
                            }
                        }
                    }
                    break;
                }

                case KEYW_BREAK:
                case KEYW_CONTINUE:
                case KEYW_RESTART: {
                    t->skipToken();
                    
                    // Identifier or anonymous loop?
                    int nextType;
                    string loopName;
                    t->peekToken(nextType, loopName);
                    if (nextType == TOKEN_IDENTIFIER) {
                        t->skipToken();
                    }
                    else {
                        loopName = blankString;
                    }
                    
                    // Find label
                    LabelMap::iterator llist = labels->find(loopName.c_str());
                    if (llist == labels->end()) {
                        if (loopName == blankString)
                            t->outputError("Must use '%s' from within a loop", token.c_str());
                        else
                            t->outputError("No label matching '%s' found ('%s' can only be used with labels that immediately precede a loop command such as 'do' or 'for')", loopName.c_str(), token.c_str());
                        // RESOLUTION: skip any remaining command even though there shouldn't be any
                        skipCmd = 1;
                        break;
                    }
                    // Get label
                    Label& label = (*llist).second.back();
                    if (!label.isLoop) {
                        t->outputError("Label '%s' does not mark a loop ('%s' can only be used with labels that immediately precede a loop command such as 'do' or 'for')", loopName.c_str(), token.c_str());
                        // RESOLUTION: skip any remaining command even though there shouldn't be any
                        skipCmd = 1;
                        break;
                    }
                    
                    // Ensure stack depth matches (shouldn't be lower- just higher)
                    compileAssert(label.stackDepth <= ws->stackDepth);
                    if (label.stackDepth < ws->stackDepth) {
                        // @TODO: Use OP_DISCARDL when we know all entries are int/float
                        postCmdI(OP_DISCARD, ws->stackDepth - label.stackDepth);
                    }
                    
                    // Jump to label
                    if (type == KEYW_RESTART) {
                        // (-2 for opcode we're adding)
                        postCmdI(OP_JUMP, label.offset - ws->code.size() - 2);
                        ended.how = ENDED_RESTART;
                    }
                    else {
                        compileAssert((type == KEYW_BREAK) || (type == KEYW_CONTINUE));
                        prepLinker(type == KEYW_CONTINUE, loopName);
                        // -1 = offset of operand from next instruction, added into final label offset
                        postCmdI(OP_JUMP, -1);
                        ended.how = (type == KEYW_CONTINUE ? ENDED_CONTINUE : ENDED_BREAK);
                    }

                    ended.scope = label.scope;
                    // Breaks any infinites up to but not including this loop
                    ended.breakInfinite = label.scope;
                    // Breaks our loop too if a break
                    if (type == KEYW_BREAK) --ended.breakInfinite;
                    
                    t->peekToken(type, token);
                    detectEndOfCommand = 1;
                    break;
                }
                
                case KEYW_DEBUG: {
                    t->skipToken();

                    // Debug output
                    Operand o = parseExpression(1);
                    if (!o.mode) {
                        skipCmd = 1;
                        break;
                    }

                    // Source already has conversion flag set if necessary, but it needs to
                    // be a 'copy' flag
                    if (!OM_IS_LITERAL(o.mode)) {
                        if (OM_IS_CONVERT(o.mode))
                            o.mode = (OperMode)(o.mode | OM_COPY);
                    }

                    // Source also needs to be const; define string literals
                    if (o.mode == OM_STR)
                        defineString(o);
                    if (OM_IS_STR(o.mode))
                        o.mode = (OperMode)(OM_STR_CONST | (o.mode & ~OM_BASETYPE));

                    // Finally, source could be a pop; temporaries should never be variants
                    if (O_IS_TEMP(o)) {
                        compileAssert(!OM_IS_ENTRY(o.mode));
                        compileAssert(o.data.i == 0);
                        o.mode = (OperMode)((o.mode & ~OM_LOCATION) | OM_POP);
                    }

                    postCmdRaw(OP_DEBUG, &o);

                    if (OM_IS_POP(o.mode))
                        --ws->stackDepth;

                    t->peekToken(type, token);
                    detectEndOfCommand = 1;
                    break;
                }
                
                case KEYW_IDLE: {
                    t->skipToken();
                    postCmd(OP_IDLE);
                    t->peekToken(type, token);
                    detectEndOfCommand = 1;
                    break;
                }

                case TOKEN_CONFIG:
                case KEYW_EXTEND:
                case KEYW_IMPORT:
                case KEYW_WITH:
                    // @TODO:

                case KEYW_FOR:
                case KEYW_FOREACH:
                case KEYW_REPEAT:
                    // @TODO: Just specialized while() loops
                    
                case KEYW_STATE:
                case KEYW_REPLY: { // @TODO: may have to peek past current block
                    // @TODO:
                    t->outputError("Not currently supported: '%s'", token.c_str());
                    skipCmd = 1;
                    break;
                }
                
                case TOKEN_ENDLINE: {
                    // Compilation should never see one of these
                    compileAssert(0);
                    break;
                }
                
                default: {
                    t->outputError("Unexpected token '%s'", token.c_str());
                    // RESOLUTION: ignore rest of command
                    skipCmd = 1;
                    break;
                }
            }
        }

        if (detectEndOfCommand) {
            if (type == TOKEN_ENDLINE) {
                t->skipToken();
            }
            else if (type != TOKEN_OPEN_BRACE) {
                t->outputError("Expected end-of-command");
                skipCmd = 1;
            }
        }
        
        if (exitBlock) break;
        // Did one command, done with non-brace block
        // Unless skipping command, then let that happen first
        if ((!braceBlock) && (scope) && (!skipCmd)) break;
    }
    
    // Missing closing brace?
    if ((braceBlock) && (!exitBlock))
        t->outputError("Missing closing brace");
        // RESOLUTION: assume brace is there

    // Remove variables created during this scope
    VariableMap::iterator edit;
    for (VariableMap::iterator pos = vars->begin(); pos != vars->end(); ) {
        edit = pos;
        ++pos;
        compileAssert(!(*edit).second.empty());
        Variable& check = (*edit).second.back();
        if (check.scope == scope) {
            // This var was created by us- remove it
            if ((check.datatype.baseType == DATA_STR) &&
                (check.datatype.flags == DATA_CONST))
                delete[] check.val.constS;
            (*edit).second.pop_back();
            if ((*edit).second.empty()) {
                // No more vars matching this name- delete
                const char* toKill = (*edit).first;
                vars->erase(edit);
                delete[] toKill;
            }
            else {
                // Should never be two vars same name same scope
                compileAssert((*edit).second.back().scope != scope);
            }
        }
    }

    // Remove labels created during this scope
    LabelMap::iterator editL;
    for (LabelMap::iterator posL = labels->begin(); posL != labels->end(); ) {
        editL = posL;
        ++posL;
        compileAssert(!(*editL).second.empty());
        Label& check = (*editL).second.back();
        if (check.scope == scope) {
            // This label was created by us- remove it
            (*editL).second.pop_back();
            if ((*editL).second.empty()) {
                // No more labels matching this name- delete
                const char* toKill = (*editL).first;
                labels->erase(editL);
                delete[] toKill;
            }
            else {
                // Should never be two labels same name same scope
                compileAssert((*editL).second.back().scope != scope);
            }
        }
    }
            
    // Main scope has a "stop"
    if (scope == 0) {
        if (!ended.how) postCmd(OP_STOP);
        ended.how = ENDED_RETURN;
        ended.scope = 0;
    }
    
    compileAssert(ws->stackDepth >= paramStackDepth);
    if (!ended.how) {
        // If end of function, return
        if (parameters) {
            Operand o;
            createEmpty(o);
            processReturn(o, returnType);
        }
        else {
            // Pop local variables
            if (ws->stackDepth > entryStackDepth) {
                // @TODO: Use OP_DISCARDL when we know all entries are int/float
                postCmdI(OP_DISCARD, ws->stackDepth - entryStackDepth);
            }
        }
    }
    
    // local variables have now been discarded
    ws->stackDepth = entryStackDepth;
    if (inFunctionScope == scope) inFunctionScope = 0;
    --scope;
}

void Compiler::processReturn(Operand& o, DataType* returnType) { start_func
    // Not in function?
    if (!inFunctionScope) {
        // (no need to destroy operand, OP_STOP clears stack)
        if (o.mode)
            t->outputError("Attempting to return value outside of function ('return' outside of a function is equivalent to 'end' and cannot return a value)");
        else
            t->outputWarning("'return' outside of a function is equivalent to 'end'");
        postCmd(OP_STOP);
    }
    else {
        // warn if return value when void
        if ((returnType == NULL) || (returnType->baseType == DATA_VOID)) {
            if (o.mode != OM_NONE) {
                t->outputError("Attempting to return value within 'void' function ('void' functions cannot return a value)");
                // RESOLUTION: destroy operand
                destroyOperand(o);
            }

            // No return value to pop
            postCmdII(OP_RETVOID, ws->stackDepth - funcParamStackDepth,
                      // (-1 for assumed code return pointer)
                      funcParamStackDepth - funcEntryStackDepth - 1);
        }
        // if nothing to return, return blank, and warn if NOT void
        else {
            if (o.mode == OM_NONE) {
                t->outputWarning("Return without value (function was not declared as 'void' and therefore should return a value)");

                generateBlank(*returnType);
                ++ws->stackDepth;
            }
            else {
                convertToMatch(o, *returnType, "return type");
                // return values can be references
                convertToTemp(o, 0, 1);
                compileAssert(o.data.i == 0);

                // Finish any pending conversions on o-
                // Anything that was a var was converted by convertToTemp, so
                // if it's still pending conversion, we can safely do it in place
                if (OM_IS_CONVERT(o.mode))
                    postCmdRaw(OP_CONVERT, &o);
            }

            // Returned value is assumed to be popped
            // (subtract now for popped return value)
            postCmdII(OP_RET, --ws->stackDepth - funcParamStackDepth,
                      // (-1 for assumed code return pointer)
                      funcParamStackDepth - funcEntryStackDepth - 1);
        }
    }
}

void Compiler::createEmpty(Operand& o) { start_func
    o.mode = OM_NONE;
    o.data.i = 0;
    o.subType = 0;
    o.flags = 0;
    o.ref.global = NULL;
    o.arrayType = 0;
}

void Compiler::createInt(Operand& o, Sint32 value) { start_func
    o.mode = OM_INT;
    o.data.i = value;
    o.subType = 0;
    o.flags = 0;
    o.ref.global = NULL;
    o.arrayType = 0;
}

void Compiler::createFloat(Operand& o, BCfloat value) { start_func
    o.mode = OM_FLOAT;
    o.data.f = value;
    o.subType = 0;
    o.flags = 0;
    o.ref.global = NULL;
    o.arrayType = 0;
}

void Compiler::appendWorkspaces(Codespace* dest, const Codespace* src) { start_func
    int size = dest->code.size();
    list<LinkEntry>::const_iterator end = src->links.end();
    for (list<LinkEntry>::const_iterator pos = src->links.begin(); pos != end; ++pos) {
        dest->links.push_back(*pos);
        dest->links.back().offset += size;
    }
    dest->code.insert(dest->code.end(), src->code.begin(), src->code.end());
}

void Compiler::checkDataType(DataType& type, int noConst, int noScope, int noVisibility, int noVoid) { start_func
    if ((noConst) && (type.flags & DATA_CONST)) {
        t->outputError("Cannot use const datatype here");
        // RESOLUTION: drop invalid datatype
        type.flags &= ~DATA_CONST;
    }
    if ((noScope) && (type.flags & DATA_LOCAL)) {
        t->outputError("Cannot use datatype scopes ('local') here");
        // RESOLUTION: drop invalid datatype
        type.flags &= ~DATA_LOCAL;
    }
    if ((noScope) && (type.flags & DATA_GLOBAL)) {
        t->outputError("Cannot use datatype scopes ('global') here");
        // RESOLUTION: drop invalid datatype
        type.flags &= ~DATA_GLOBAL;
    }
    if ((noVisibility) && (type.flags & DATA_PRIVATE)) {
        t->outputError("Cannot use visibility modifiers ('private') here");
        // RESOLUTION: drop invalid datatype
        type.flags &= ~DATA_PRIVATE;
    }
    if ((noVisibility) && (type.flags & DATA_PUBLIC)) {
        t->outputError("Cannot use visibility modifiers ('public') here");
        // RESOLUTION: drop invalid datatype
        type.flags &= ~DATA_PUBLIC;
    }
    if ((noVoid) && (type.baseType == DATA_VOID)) {
        t->outputError("Cannot use 'void' datatype here");
        // RESOLUTION: change to variant
        type.baseType = DATA_VAR;
    }
}

Compiler::Operand Compiler::parseFunction(const Function& function, const string& funcName) { start_func
    // @TODO: only supports local functions so far (no global/built-in/sends/queries)
    int paramNum = 0;
    int type;
    string token;

    // Optional paren
    int paren = 0;
    t->peekToken(type, token);
    if (type == TOKEN_OPEN_PAREN) {
        paren = 1;
        t->skipToken();
        t->peekToken(type, token);
    }

    // Parameters? these tokens MOSTLY match those in parseoperand-
    // Exceptions- don't recognize +, - as starting an operand if no parens
    if ((type == TOKEN_INTEGER) || (type == TOKEN_HEX) || (type == TOKEN_DECIMAL) ||
        (type == TOKEN_STRING) || (type == KEYW_TRUE) || (type == KEYW_FALSE) ||
        (type == KEYW_QUERY) || (type == TOKEN_IDENTIFIER) || (type == KEYW_ALL) ||
        (type == KEYW_ALL_OTHER) || (type == KEYW_THIS) || (type == KEYW_SOURCE) ||
        (type == KEYW_DEFAULT) || (type == OPER_B_NOT) || (type == OPER_L_NOT) ||
        (type == KEYW_NOTHING) ||
        ((paren) && ((type == OPER_PLUS) || (type == OPER_MINUS)))) {
        // Parse operands from here on
        do {
            Operand o = parseExpression();
            if (!o.mode) break;
            
            // Convert to proper operand type
            // (can't convert if no more param types known)
            if (paramNum < function.numparam)
                convertToMatch(o, function.parameters[paramNum], "parameter");
            ++paramNum;
            // (parameters can be references)
            convertToTemp(o, 0, 1);
            // (above ensures any string has been deleted)
            
            // Finish any pending conversions on o-
            // Anything that was a var was converted by convertToTemp, so
            // if it's still pending conversion, we can safely do it in place
            if (OM_IS_CONVERT(o.mode))
                postCmdRaw(OP_CONVERT, &o);

            // Another parameter?
            t->peekToken(type, token);
            if (type == TOKEN_COMMA) t->skipToken();
        } while (type == TOKEN_COMMA);
    }
    
    // Incorrect number of parameters?
    if (paramNum != function.numparam) {
        t->outputError("Incorrect number of parameters to function (%d were given; %d were expected)", paramNum, function.numparam);
        // RESOLUTION: continue on; code is of course unusable
    }
    
    // End paren? (already peeked previously)
    if (paren) {
        if (type == TOKEN_CLOSE_PAREN)
            t->skipToken();
        else
            t->outputError("Missing closing parenthesis to match open parenthesis");
            // RESOLUTION: pretend paren was there
    }
    
    // Parameters are on stack now
    prepLinker(funcName);
    // -1 = offset of operand compared to next opcode; added into final function offset
    postCmdI(OP_SUBR, -1);
    
    // "Pop" parameters, push return value if any
    if (function.returntype.baseType == DATA_VOID) {
        ws->stackDepth -= paramNum;
        Operand o;
        createInt(o, 0);
        o.flags = Operand::OF_IS_VOID;
        return o;
    }
    else {
        ws->stackDepth -= paramNum - 1;

        // Return value is on stack now, type is known
        Operand o = dataTypeToOperand(function.returntype);
        o.mode = (OperMode)(o.mode | OM_STACK);
        return o;
    }
}

Compiler::Operand Compiler::dataTypeToOperand(DataType dt) { start_func
    Operand o;
    o.flags = 0;
    o.data.i = 0;
    o.ref.global = NULL;
    o.mode = (OperMode)dt.baseType;
    if (dt.baseType == DATA_VAR)
        o.mode = OM_ENTRY;
    if ((o.mode == OM_ENTITY) || (o.mode == OM_OBJECT))
        o.subType = dt.subType;
    if (dt.flags & DATA_ARRAY) {
        o.arrayType = o.mode;
        o.mode = OM_ARRAY;
    }
    else if (dt.flags & DATA_HASH) {
        o.arrayType = o.mode;
        o.mode = OM_HASH;
    }
    o.mode = (OperMode)(o.mode | OM_NO_CONVERT);
    return o;
}

int Compiler::findFunction(const string& name, Function& function) const { start_func
    // find matching local function
    FunctionMap::iterator flist = func->find(name.c_str());
    if (flist == func->end()) {
        // @TODO: also need to check global functions and built-ins
        return 0;
    }
    
    // @TODO: optimize self/tail recursion?
    function = (*flist).second;
    return 1;
}

void Compiler::convertToMatch(Operand& o, DataType datatype, const char* errorTarget) { start_func
    compileAssert(!OM_IS_NONE(o.mode));
    
    // Ensure data types match- convert or error if needed
    // Check array/hash first because target could be var hash/array
    if ((datatype.flags & DATA_ARRAY) || (datatype.flags & DATA_HASH)) {
        string defined = "array";
        int isType = OM_IS_ARRAY(o.mode);
        if (datatype.flags & DATA_HASH) {
            defined = "hash";
            isType = OM_IS_HASH(o.mode);
        }
        if (isType) {
            if ((o.arrayType != datatype.baseType) && (o.arrayType != DATA_VAR) && (datatype.baseType != DATA_VAR)) {
                t->outputError("Incorrect %s type- does not match %s", defined.c_str(), errorTarget);
            }
            else if ((o.arrayType == DATA_ENTITY) && (datatype.baseType == DATA_ENTITY)) {
                // Ensure same subtype or converting to no type
                if ((o.subType != datatype.subType) && (datatype.subType))
                    t->outputError("Incorrect %s type- script type does not match %s", defined.c_str(), errorTarget);
            }
            else if ((o.arrayType == DATA_OBJECT) && (datatype.baseType == DATA_OBJECT)) {
                // Ensure same subtype
                if (o.subType != datatype.subType)
                    t->outputError("Incorrect %s type- object type does not match %s", defined.c_str(), errorTarget);
            }
        }
        else if (!OM_IS_ENTRY(o.mode)) {
            t->outputError("Incorrect data type- %s must be assigned %s", errorTarget, defined.c_str());
        }
        // RESOLUTION: converts anyways
        if (datatype.flags & DATA_ARRAY) convertToArray(o, datatype);
        else convertToHash(o, datatype);
    }
    // Anything else, no need to convert to var
    else if (datatype.baseType != DATA_VAR) {
        if (datatype.baseType == DATA_INT) {
            if (!OM_IS_INT(o.mode) && !OM_IS_FLOAT(o.mode) && !OM_IS_ENTRY(o.mode))
                t->outputError("Cannot automatically convert to integer here");
            // RESOLUTION: converts anyways
            convertToInt(o);
        }
        else if (datatype.baseType == DATA_FLOAT) {
            if (!OM_IS_INT(o.mode) && !OM_IS_FLOAT(o.mode) && !OM_IS_ENTRY(o.mode))
                t->outputError("Cannot automatically convert to floating point here");
            // RESOLUTION: converts anyways
            convertToFloat(o);
        }
        else if (datatype.baseType == DATA_STR) {
            if (!OM_IS_INT(o.mode) && !OM_IS_FLOAT(o.mode) && !OM_IS_STR(o.mode) && !OM_IS_ENTRY(o.mode))
                t->outputError("Cannot automatically convert to string here");
            // RESOLUTION: converts anyways
            convertToStr(o);
        }
        else if (datatype.baseType == DATA_ENTITY) {
            if (OM_IS_LITERAL(o.mode)) {
                if ((o.mode != OM_ALL) && (o.mode != OM_ALL_OTHER) &&
                    (o.mode != OM_NOTHING) && (o.mode != OM_THIS))
                    t->outputError("Cannot automatically convert to entity object here");
            }
            else {
                if ((o.mode & OM_BASETYPE) != DATA_ENTITY)
                    t->outputError("Cannot automatically convert to entity object here");
                // Ensure same subtype or converting to no type
                else if ((o.subType != datatype.subType) && (datatype.subType))
                    t->outputError("Incorrect script type- does not match %s", errorTarget);
            }
            // RESOLUTION: converts anyways
            convertToObject(o, datatype);
        }
        else {
            // All other object types
            assert(datatype.baseType == DATA_OBJECT);
            if (OM_IS_LITERAL(o.mode)) {
                if (o.mode != OM_NOTHING)
                    t->outputError("Cannot automatically convert to object type here");
            }
            else {
                if ((o.mode & OM_BASETYPE) != DATA_OBJECT)
                    t->outputError("Cannot automatically convert to object type here");
                // Ensure same subtype
                else if (o.subType != datatype.subType)
                    t->outputError("Incorrect object type- does not match %s", errorTarget);
            }
            // RESOLUTION: converts anyways
            convertToObject(o, datatype);
        }
    }
}

void Compiler::parseAssignment(Operand& var, DataType varType, const Variable* varToCount, int memberPos) { start_func
    // @TODO: calculate directly into variable instead of temporary?
    // (only if no possibility of variable self-referenced in expression)
    // use same algorithm as for a = a + 1 below
    // @TODO: optimize a = a + 1 (etc.) by preparsing and counting uses of a; then
    // during expression parsing, the last time you encounter it, you can now modify
    // it directly; for non-scoped vars, you must count function calls as a possible 'use'.
    // Mark this in operand somehow, and treat it as an equal to a temporary in many places (be careful of type matching)
    // OPERATOR++ and inline-assignment potentially affect this algorithm.
    // This optimization must be ignored if OPERATOR+= is being used.
    // @TODO: prevent use of variable in it's own initialization
    // (just don't define it before calling this?)
    // @TODO: a = b + 1 should be (store b into a, += 1) not (push b, += 1, pop into a)
    // if preparse above finds NO uses of variable, then first time something would be pushed,
    // store into variable instead and then use that as a temporary (only if type matches)
    // @TODO: doesn't work with . (as . isn't coded yet) or probably with 'as'
    
    // Preparse the expression for uses of variable, or functions that MIGHT affect variable
    // This count allows us to optimize the expression once all other uses of the variable
    // are out of the way by doing further operations directly within the variable
    
/*
    int bknum = t->getBookmarkName();
    t->bookmarkStore(bknum);
    t->silentErrors(1);
    Operand counted = parseExpression(0, 1, NULL, 100, &var);
    t->silentErrors(0);
    if (varToCount) {
        t->bookmarkCancel(bknum);
        return;
    }
    t->bookmarkReturn(bknum);
debugWrite("Found %d instances of %s", counted.data.i, var.name);
*/

    // The easy part- parse the expression
    Operand o = parseExpression();
    if (!o.mode) return;

    compileAssert(!OM_IS_POP(o.mode));
    compileAssert(!OM_IS_COPY(o.mode));
        
    // @TODO: should error if storing to a const

    convertToMatch(o, varType, "variable");

    // Permutations:
    //   known into known, same type
    //   known (conversion needed) into known, same type
    //   known into variant (convert variant)
    //   known (conversion needed) into variant (convert variant)
    //   variant into variant

    // Default to object, as that covers multiple types and literals
    Opcode opc = OP_STOREo;
    if (memberPos >= 0) {
        assert(!OM_IS_ENTRY(o.mode));
        if (OM_IS_ENTITY(var.mode)) opc = OP_SETe;
        else {
            compileAssert(OM_IS_OBJECT(var.mode));
            opc = OP_SETo;
        }
    }
    else if (OM_IS_STR(o.mode)) opc = OP_STOREs;
    else if (OM_IS_FLOAT(o.mode)) opc = OP_STOREf;
    else if (OM_IS_INT(o.mode)) opc = OP_STORE;
    else if (OM_IS_ARRAY(o.mode)) opc = OP_STOREa;
    else if (OM_IS_HASH(o.mode)) opc = OP_STOREh;
    else if (!OM_IS_LITERAL(o.mode)) {
        if (OM_IS_ENTRY(o.mode)) {
            compileAssert(varType.baseType == DATA_VAR);
            // @TODO: ensure storev does ALL ref/deref needed, conversions, and copying new array/hash
            opc = OP_STOREv;
        }
    }
    
    // Dest is set to 'no convert' which is correct unless storing to a variant
    // from a non-variant
    if ((varType.baseType == DATA_VAR) && (opc != OP_STOREv))
        var.mode = (OperMode)(var.mode & ~OM_NO_CONVERT);
    
    // Source already has conversion flag set if necessary, but it needs to
    // be a 'copy' flag
    if (!OM_IS_LITERAL(o.mode)) {
        if (OM_IS_CONVERT(o.mode))
            o.mode = (OperMode)(o.mode | OM_COPY);
        // Force a copy if non-unique array/hash
        else if (OM_IS_KNOWN(o.mode) && (OM_IS_ARRAY(o.mode) || OM_IS_HASH(o.mode)) && !O_IS_COPY(o) && (opc != OP_STOREv))
            o.mode = (OperMode)((o.mode & ~OM_FLAGS) | OM_COPY);
    }
        
    // Source also needs to be const; define string literals
    if (o.mode == OM_STR)
        defineString(o);
    if (OM_IS_STR(o.mode))
        o.mode = (OperMode)(OM_STR_CONST | (o.mode & ~OM_BASETYPE));

    // Finally, source could be a pop; temporaries should never be variants
    if (O_IS_TEMP(o)) {
        compileAssert(!OM_IS_ENTRY(o.mode));
        compileAssert(o.data.i == 0);
        o.mode = (OperMode)((o.mode & ~OM_LOCATION) | OM_POP);
    }
    
    // 'var' goes after 'o' in case both are POPs- popping happens in right order
    if ((opc == OP_SETo) || (opc == OP_SETe)) {
        // Specialized versions
        if (OM_IS_FLOAT(o.mode)) opc = (opc == OP_SETo) ? OP_SETof : OP_SETef;
        else if (OM_IS_INT(o.mode)) opc = (opc == OP_SETo) ? OP_SEToi : OP_SETei;

        Operand b;
        createInt(b, memberPos);
        postCmdRaw(opc, &o, &var, &b);
    }
    else
        postCmdRaw(opc, &o, &var);
    var.mode = (OperMode)(var.mode | OM_NO_CONVERT);
    
    if (OM_IS_POP(o.mode))
        --ws->stackDepth;
}

Compiler::Operand Compiler::variableToOperand(const Variable& var) { start_func
    Operand result;
    
    if (var.datatype.flags & DATA_CONST) {
        // Constants
        result.arrayType = 0;
        result.flags = 0;
        result.subType = 0;
        result.mode = (OperMode)var.datatype.baseType;
        result.ref.global = NULL;
        if (var.datatype.baseType == DATA_INT) {
            result.data.i = var.val.constI;
        }
        else if (var.datatype.baseType == DATA_FLOAT) {
            result.data.f = var.val.constF;
        }
        else {
            compileAssert(var.datatype.baseType == DATA_STR);
            result.data.i = 0;
            result.ref.strLiteral = new string(var.val.constS);
        }
    }
    else {
        result = dataTypeToOperand(var.datatype);
        result.flags |= Operand::OF_IS_VAR;
        if (var.scope == 0) {
            result.mode = (OperMode)(result.mode | OM_LOCAL);
            result.data.i = var.val.offs;
        }
        else if (var.scope > 0) {
            result.mode = (OperMode)(result.mode | OM_STACK);
            result.data.i = var.val.offs + ws->stackDepth;
        }
        else {
            result.mode = (OperMode)(result.mode | OM_GLOBAL);
            result.ref.global = &var;
            result.data.i = var.val.offs;
        }
    }

    return result;
}

int Compiler::convertToTemp(Operand& o, int partialConvert, int refOK) { start_func
    int redoString = 0;
    if (partialConvert) compileAssert(OM_IS_LITERAL(o.mode));
    
    if (!O_IS_TEMP(o)) {
        // Create temporary via PUSH
        if (o.mode == OM_STR) {
            if (partialConvert) {
                redoString = 1;
                // We NEED to be sure this is zero because we finish conversion
                // later via an addition to the operand created
                compileAssert(o.data.i == 0);
            }
            else defineString(o);
        }
        Operand copyOfO = o;
        postCmdPush(copyOfO, refOK);
        ++ws->stackDepth;
        // Modify operand to match new position
        o.flags = 0;
        if (o.mode != OM_STR) o.ref.global = NULL;
        if (!refOK) o.flags = Operand::OF_IS_COPY;
        o.mode = (OperMode)((o.mode & ~OM_LOCATION) | OM_STACK);
        if (OM_IS_CONVERT(o.mode)) o.mode = (OperMode)((o.mode & ~OM_FLAGS) | OM_NO_CONVERT);
        if (!partialConvert) o.data.i = 0;
    }
    
    return redoString;
}

void Compiler::convertToInt(Operand& o) { start_func
    compileAssert(o.mode);
    compileAssert(!OM_IS_COPY(o.mode));
    compileAssert(!OM_IS_CONST(o.mode));
    compileAssert(!OM_IS_POP(o.mode));
    
    // Already int
    if (OM_IS_INT(o.mode)) return;

    // Main possibilities-
    //   Literal- convert to another literal
    //   Otherwise- mark as 'needed conversion'

    if (OM_IS_LITERAL(o.mode)) {
        if (o.mode == OM_FLOAT) {
            o.data.i = (Sint32)o.data.f;
        }
        else if (o.mode == OM_STR) {
            o.data.i = strToInt(*o.ref.strLiteral);
            delete o.ref.strLiteral;
            o.ref.strLiteral = NULL;
        }
        else {
            o.data.i = 0;
            o.subType = 0;
            o.arrayType = 0;
        }
        o.mode = OM_INT;
        return;
    }
    
    // (clears no_convert flag also)
    o.mode = (OperMode)((o.mode & OM_LOCATION) | OM_INT);
}

void Compiler::convertToFloat(Operand& o) { start_func
    compileAssert(o.mode);
    compileAssert(!OM_IS_COPY(o.mode));
    compileAssert(!OM_IS_CONST(o.mode));
    compileAssert(!OM_IS_POP(o.mode));
    
    // Already float
    if (OM_IS_FLOAT(o.mode)) return;

    // Main possibilities-
    //   Literal- convert to another literal
    //   Otherwise- mark as 'needed conversion'

    if (OM_IS_LITERAL(o.mode)) {
        if (o.mode == OM_INT) {
            o.data.f = (BCfloat)o.data.i;
        }
        else if (o.mode == OM_STR) {
            o.data.f = strToFloat(*o.ref.strLiteral);
            delete o.ref.strLiteral;
            o.ref.strLiteral = NULL;
        }
        else {
            o.data.f = 0.0;
            o.subType = 0;
            o.arrayType = 0;
        }
        o.mode = OM_FLOAT;
        return;
    }
    
    // (clears no_convert flag also)
    o.mode = (OperMode)((o.mode & OM_LOCATION) | OM_FLOAT);
}

void Compiler::convertToStr(Operand& o) { start_func
    compileAssert(o.mode);
    compileAssert(!OM_IS_COPY(o.mode));
    compileAssert(!OM_IS_CONST(o.mode));
    compileAssert(!OM_IS_POP(o.mode));
    
    // Already string
    if (OM_IS_STR(o.mode)) return;

    // Main possibilities-
    //   Literal- convert to another literal
    //   Otherwise- mark as 'needed conversion'

    if (OM_IS_LITERAL(o.mode)) {
        o.data.i = 0;
        if (o.mode == OM_INT) {
            o.ref.strLiteral = new string(intToStr(o.data.i));
        }
        else if (o.mode == OM_FLOAT) {
            o.ref.strLiteral = new string(floatToStr(o.data.f));
        }
        else {
            o.ref.strLiteral = new string(blankString);
            o.subType = 0;
            o.arrayType = 0;
        }
        o.mode = OM_STR;
        return;
    }
    
    // (clears no_convert flag also)
    o.mode = (OperMode)((o.mode & OM_LOCATION) | OM_STR);
}

void Compiler::convertToEntity(Operand& o, DataType type) { start_func
    compileAssert(o.mode);
    compileAssert(!OM_IS_COPY(o.mode));
    compileAssert(!OM_IS_CONST(o.mode));
    compileAssert(!OM_IS_POP(o.mode));
    compileAssert(type.baseType == DATA_ENTITY);

    // Already entity- check for proper type
    if (OM_IS_ENTITY(o.mode) && !OM_IS_LITERAL(o.mode)) {
        // any type matches no-subtype
        if (!type.subType) return;
        // Otherwise type must match exactly
        if (type.subType == o.subType) return;
        // Type failed to match- will mark for conversion later
    }

    // Main possibilities-
    //   Literal- convert to another literal
    //   Otherwise- mark as 'needed conversion'

    if (OM_IS_LITERAL(o.mode)) {
        // Certain literals are ok
        if ((o.mode == OM_ALL) || (o.mode == OM_ALL_OTHER) || (o.mode == OM_NOTHING)) {
            o.subType = type.subType;
            return;
        }
        
        if ((o.mode == OM_THIS) && ((type.subType == scriptType) || (!type.subType)))
            return;

        if (o.mode == OM_STR) {
            delete o.ref.strLiteral;
            o.ref.strLiteral = NULL;
        }
        
        // Create a 'nothing' entity
        o.mode = OM_NOTHING;
        o.arrayType = 0;
        o.subType = type.subType;
        o.data.i = 0;
        o.flags = 0;
        return;
    }
    
    // (clears no_convert flag also)
    o.mode = (OperMode)((o.mode & OM_LOCATION) | OM_ENTITY);
    o.subType = type.subType;
}

void Compiler::convertToObject(Operand& o, DataType type) { start_func
    compileAssert(o.mode);
    compileAssert(!OM_IS_COPY(o.mode));
    compileAssert(!OM_IS_CONST(o.mode));
    compileAssert(!OM_IS_POP(o.mode));
    compileAssert(type.baseType == DATA_OBJECT);
    compileAssert(type.subType);

    // Already object- check for proper type
    if (OM_IS_OBJECT(o.mode) && !OM_IS_LITERAL(o.mode)) {
        // Type must match exactly
        if (type.subType == o.subType) return;
        // Type failed to match- will mark for conversion later
    }

    // Main possibilities-
    //   Literal- convert to another literal
    //   Otherwise- mark as 'needed conversion'

    if (OM_IS_LITERAL(o.mode)) {
        // Certain literals are ok
        if (o.mode == OM_NOTHING) {
            o.subType = type.subType;
            return;
        }

        if (o.mode == OM_STR) {
            delete o.ref.strLiteral;
            o.ref.strLiteral = NULL;
        }
        
        // Create a 'nothing' object
        o.mode = OM_NOTHING;
        o.arrayType = 0;
        o.subType = type.subType;
        o.data.i = 0;
        o.flags = 0;
        return;
    }
    
    // (clears no_convert flag also)
    o.mode = (OperMode)((o.mode & OM_LOCATION) | OM_OBJECT);
    o.subType = type.subType;
}

void Compiler::convertToArray(Operand& o, DataType type) { start_func
    // @TODO: remember that when this is used, a simple copy bit won't
    // complete a specific array type conversion- just promise it's an
    // array of SOME type; you must specifically use a conversion opcode
    // or assume you are pulling a variant out. WE CAN CHANGE THIS by
    // pushing array+subtype whenever conversion/copy operands are used
    assert(0);

    compileAssert(o.mode);
    compileAssert(!OM_IS_COPY(o.mode));
    compileAssert(!OM_IS_CONST(o.mode));
    compileAssert(!OM_IS_POP(o.mode));
    compileAssert(type.flags & DATA_ARRAY);

    // Already array- check for proper type
    if (OM_IS_ARRAY(o.mode)) {
        // any type matches variant type
        if (type.baseType == DATA_VAR) return;
        // Otherwise type must match exactly
        if (o.arrayType == type.baseType) {
            if ((type.baseType != DATA_ENTITY) && (type.baseType != DATA_OBJECT)) return;
            // Match subtype exactly for entities/objects
            if (type.subType == o.subType) return;
        }
        // Type failed to match- will mark for conversion later
    }

    // Main possibilities-
    //   Literal- convert to another literal
    //   Otherwise- mark as 'needed conversion'

    if (OM_IS_LITERAL(o.mode)) {
        if (o.mode == OM_STR) {
            delete o.ref.strLiteral;
            o.ref.strLiteral = NULL;
        }
            
        // Create a new array- if variant type, create an INT array
        o.mode = (OperMode)(OM_ARRAY | OM_STACK | OM_NO_CONVERT);
        o.arrayType = type.baseType;
        o.subType = 0;
        o.data.i = 0;
        o.flags = Operand::OF_IS_COPY;
        
        if (type.baseType == DATA_VAR) {
            postCmdII(OP_CREATEa, DATA_INT, 0);
            o.arrayType = DATA_INT;
        }
        else {
            postCmdII(OP_CREATEa, type.baseType,
                      (type.baseType == DATA_ENTITY ||
                       type.baseType == DATA_OBJECT) ? type.subType : 0);
        }

        return;
    }
    
    // (clears no_convert flag also)
    // If was a copy before, would still be; if not, is not, so don't change flags
    o.mode = (OperMode)((o.mode & OM_LOCATION) | OM_ARRAY);
    o.subType = type.subType;
    o.arrayType = type.baseType;
    if (type.baseType = DATA_VAR) o.arrayType = OM_INT;
}

void Compiler::convertToHash(Operand& o, DataType type) { start_func
    // @TODO: remember that when this is used, a simple copy bit won't
    // complete a specific hash type conversion- just promise it's an
    // array of SOME time; you must specifically use a conversion opcode
    // or assume you are pulling a variant out. WE CAN CHANGE THIS by
    // pushing hash+subtype whenever conversion/copy operands are used
    assert(0);

    compileAssert(o.mode);
    compileAssert(!OM_IS_COPY(o.mode));
    compileAssert(!OM_IS_CONST(o.mode));
    compileAssert(!OM_IS_POP(o.mode));
    compileAssert(type.flags & DATA_HASH);

    // Already hash- check for proper type
    if (OM_IS_HASH(o.mode)) {
        // any type matches variant type
        if (type.baseType == DATA_VAR) return;
        // Otherwise type must match exactly
        if (o.arrayType == type.baseType) {
            if ((type.baseType != DATA_ENTITY) && (type.baseType != DATA_OBJECT)) return;
            // Match subtype exactly for entities/objects
            if (type.subType == o.subType) return;
        }
        // Type failed to match- will mark for conversion later
    }

    // Main possibilities-
    //   Literal- convert to another literal
    //   Otherwise- mark as 'needed conversion'

    if (OM_IS_LITERAL(o.mode)) {
        if (o.mode == OM_STR) {
            delete o.ref.strLiteral;
            o.ref.strLiteral = NULL;
        }
            
        // Create a new hash- if variant type, create an INT hash
        o.mode = (OperMode)(OM_HASH | OM_STACK | OM_NO_CONVERT);
        o.arrayType = type.baseType;
        o.subType = 0;
        o.data.i = 0;
        o.flags = Operand::OF_IS_COPY;
        
        if (type.baseType == DATA_VAR) {
            postCmdII(OP_CREATEh, DATA_INT, 0);
            o.arrayType = DATA_INT;
        }
        else {
            postCmdII(OP_CREATEh, type.baseType,
                      (type.baseType == DATA_ENTITY ||
                       type.baseType == DATA_OBJECT) ? type.subType : 0);
        }

        return;
    }
    
    // (clears no_convert flag also)
    // If was a copy before, would still be; if not, is not, so don't change flags
    o.mode = (OperMode)((o.mode & OM_LOCATION) | OM_HASH);
    o.subType = type.subType;
    o.arrayType = type.baseType;
    if (type.baseType = DATA_VAR) o.arrayType = OM_INT;
}

void Compiler::destroyOperand(Operand& o) { start_func
    compileAssert(!OM_IS_POP(o.mode));
    if (!o.mode)
        return;
    if (o.mode == OM_STR) {
        delete o.ref.strLiteral;
        o.ref.strLiteral = NULL;
    }
    if (O_IS_TEMP(o)) {
        compileAssert(o.data.i == 0);
        if (OM_IS_INT(o.mode) || OM_IS_FLOAT(o.mode)) postCmdI(OP_DISCARDL, 1);
        else postCmdI(OP_DISCARD, 1);
        --ws->stackDepth;
    }
}

int Compiler::precedence(int oper) { start_func
    // (for error states)
    if (oper == 0) return 0;

    compileAssert(oper & TOKEN_OPERATOR);
    switch (oper) {
        case OPER_MEMBER: // (presumed to have lowest precedence)
        case OPER_AS:
            return 2;
            
        case OPER_B_NOT:
        case OPER_L_NOT:
            return 3;
    
        case OPER_MULT:
        case OPER_DIV:
        case OPER_MOD:
            return 4;
            
        case OPER_PLUS:
        case OPER_MINUS:
            return 5;
            
        case OPER_LSHIFT:
        case OPER_RSHIFT:
            return 6;

        case OPER_B_AND:
        case OPER_B_OR:
        case OPER_B_XOR:
            return 7;

        case OPER_CONCAT:
            return 8;
            
        case OPER_LT:
        case OPER_LE:
        case OPER_GT:
        case OPER_GE:
            return 9;

        case OPER_EQ:
        case OPER_NE:
        case OPER_IS:
            return 10;

        case OPER_L_AND:
        case OPER_L_OR:
        case OPER_L_XOR:
            return 11;
            
        case OPER_ASSIGN:
            return 12;
    }
    
    compileAssert(0);
    return 1;
}

Compiler::Operand Compiler::parseExpression(int warnAssign, int usingValue, Operand* firstOperand, int stopPrecedence, const Variable* varToCount) { start_func
    int type;
    string token;
    Operand a;

    if (firstOperand) a = *firstOperand;
    else a = parseOperand(warnAssign, usingValue, varToCount);
    if (a.mode == 0) {
        return a;
    }
    
    t->peekToken(type, token);
    
    while (1) {
        size_t aFinishLiteralAt = 0;
        size_t aFinishLiteralSize = 0;
        int aFinishString = 0;
    
        if (type & TOKEN_OPERATOR) {
            if (O_IS_VOID(a)) {
                t->outputError("Function does not return a value (function returns 'void' and cannot be used within an expression)");
                // RESOLUTION: proceed with int constant
                a.flags &= ~Operand::OF_IS_VOID;
            }
            if (type == OPER_IS) {
                // Second operand can be a datatype or scriptname
                // @TODO:
                t->outputError("Not currently supported: '%s'", token.c_str());
            }
            else if (type == OPER_AS) {
                // Second operand must be a datatype or scriptname
                // @TODO:
                t->outputError("Not currently supported: '%s'", token.c_str());
            }
            else if ((type == OPER_B_NOT) || (type == OPER_L_NOT)) {
                t->outputError("Unexpected operator '%s' (this operator only takes one operand and should not appear between two values)", token.c_str());
                // RESOLUTION: Start expression anew at this unary operator for (hopefully) proper expression parsing
                return parseExpression(warnAssign, usingValue, NULL, stopPrecedence, varToCount);
            }
            else if (type == OPER_ASSIGN) {
                // Should've been handled in operand parsing
                t->outputError("Cannot assign here- can only assign to a variable or subscripted variable");
                // RESOLUTION: treat like == in order to proceed
                type = OPER_EQ;
            }
            
            // Anything else- proceed normally
            // Non-commutative non-reversible operators (- / % << >> #) require the
            // first operand to be a temporary. We hold off on defining strings as
            // we may cancel this if a and b end up being constants
            // (= . 'as' 'is' are non-commutative but not applicable here)
            if (((type == OPER_MINUS) || (type == OPER_DIV) || (type == OPER_MOD) ||
                 (type == OPER_LSHIFT) || (type == OPER_RSHIFT) || (type == OPER_CONCAT)) &&
                 !varToCount) {
                if (OM_IS_LITERAL(a.mode)) aFinishLiteralAt = ws->code.size();
                aFinishString = convertToTemp(a, aFinishLiteralAt);
                aFinishLiteralSize = ws->code.size();
            }
            
            // Do type-check for . operator now, to prevent repeated checks later
            if (type == OPER_MEMBER) {
                if (!OM_IS_ENTRY(a.mode) && !OM_IS_STR(a.mode) && a.mode != OM_ALL &&
                    a.mode != OM_ALL_OTHER && a.mode != OM_THIS &&
                    (OM_IS_LITERAL(a.mode) || (!OM_IS_ENTITY(a.mode) && !OM_IS_OBJECT(a.mode)))) {
                    t->outputError("Left operand to . must be an entity or object or something that can be used as one");
                    // RESOLUTION: continue at next operand
                    t->skipToken();
                    return parseOperand(warnAssign, usingValue, varToCount);
                }
            }
        }
        else {
            // Not an error- no recognized operator means a single operand- just return it
            return a;
        }
        t->skipToken();

        // The first non-. operator means we're now "using" return values.
        if (type != OPER_MEMBER) usingValue = 1;
        Operand b = parseOperand(warnAssign, usingValue, varToCount, type == OPER_MEMBER ? &a : NULL);
        if (b.mode == 0)
            return a;
            
        // If parseOperand did all the work of the OPER_MEMBER, then a.mode will be 0 now

        // Now peek ahead for another operator
        int nextType;
        string nextToken;
        t->peekToken(nextType, nextToken);

        // @TODO: future optimization, if operator is same and is
        //        commutative/associative (+, *) and is more likely to be const+const,
        //        yield precedence to help precalculate constants
        
        // Compare precedence
        if ((nextType & TOKEN_OPERATOR) && (type != OPER_MEMBER) &&
            (precedence(type) > precedence(nextType))) {
            // Next expression has precedence for now
            b = parseExpression(warnAssign, 1, &b, precedence(type), varToCount);
            if (b.mode == 0) {
                // Even though an error condition, we need to finish temporarization
                if (aFinishLiteralAt) {
                    compileAssert(!varToCount);
                    
                    if (aFinishString) {
                        a.mode = OM_STR;
                        defineString(a);
                        a.mode = (OperMode)(OM_STR | OM_STACK | OM_NO_CONVERT);
                        // We know that a string push would've been 2 ints total
                        ws->code[aFinishLiteralAt + 1] += a.data.i;
                    }
                    a.data.i = 0;
                }
                return a;
            }
            
            // (peek again)
            t->peekToken(nextType, nextToken);
        }
        
        // Now we have precedence.
        
        // If a was a temporarized literal...
        if (aFinishLiteralAt) {
            compileAssert(!varToCount);
                    
            // We have to finish OR cancel temporarization of a
            if (OM_IS_LITERAL(b.mode)) {
                // Cancel- nothing else should have been placed on stack!
                compileAssert(ws->code.size() == aFinishLiteralSize);
                // We can't be positive that exactly 2 ints were added to bytecode,
                // so calculate difference and remove that much
                while (aFinishLiteralSize != aFinishLiteralAt) {
                    ws->code.pop_back();
                    --aFinishLiteralSize;
                }
                --ws->stackDepth;
                a.flags = 0;
                // Clears location and OM_NO_CONVERT
                a.mode = (OperMode)(a.mode & OM_BASETYPE);
            }
            else {
                // Finish
                if (aFinishString) {
                    a.mode = OM_STR;
                    defineString(a);
                    a.mode = (OperMode)(OM_STR | OM_STACK | OM_NO_CONVERT);
                    // We know that a string push would've been 2 ints total
                    ws->code[aFinishLiteralAt + 1] += a.data.i;
                }
                a.data.i = 0;
            }
        }
            
        // Operation
        if (a.mode == 0)
            a = b;
        else if (varToCount)
            a.data.i += b.data.i;
        else
            a = parseOperation(type, token, a, b);
            
        // Loop back for more? Don't loop back if next operator
        // has worse precedence than our caller
        if ((nextType & TOKEN_OPERATOR) &&
            (stopPrecedence > precedence(nextType))) {
            type = nextType;
            token = nextToken;
            continue;
        }
            
        // Done
        return a;
    }
}

Compiler::Operand Compiler::parseOperand(int warnAssign, int usingValue, const Variable* varToCount, Operand* memberOf) { start_func
    int type;
    string token;
    Operand result;
    createEmpty(result);
    int skippedToken = 0;

    if (t->peekToken(type, token)) {
        // REMEMBER WHEN ADDING NEW OPERAND TOKENS THAT THESE NEED
        // TO BE ADDED TO PARSEFUNCTION() AS INITIATING TOKENS ALSO
        // AND ALSO TO MAIN COMPILEBLOCK() LOOP AS EXPRESSION TOKENS
        switch (type) {
            case TOKEN_INTEGER: {
                if (varToCount) { createInt(result, 0); break; }
                result.data.i = strToInt(token);
                result.mode = OM_INT;
                break;
            }
            
            case TOKEN_HEX: {
                if (varToCount) { createInt(result, 0); break; }
                result.data.i = strtol(token.c_str(), NULL, 0);
                result.mode = OM_INT;
                break;
            }
            
            case TOKEN_DECIMAL: {
                if (varToCount) { createInt(result, 0); break; }
                result.data.f = strToFloat(token);
                result.mode = OM_FLOAT;
                break;
            }
            
            case TOKEN_STRING: {
                if (varToCount) { createInt(result, 0); break; }
                result.data.i = 0;
                result.ref.strLiteral = new string(token);
                result.mode = OM_STR;
                break;
            }
            
            // @TTODO: Consolidate these constants?
            case KEYW_TRUE: {
                if (varToCount) { createInt(result, 0); break; }
                result.data.i = 1;
                result.mode = OM_INT;
                break;
            }
            
            case KEYW_FALSE: {
                if (varToCount) { createInt(result, 0); break; }
                result.data.i = 0;
                result.mode = OM_INT;
                break;
            }
            
            case KEYW_QUERY: {
                if (varToCount) { createInt(result, 0); break; }
                result.data.i = -1;
                result.mode = OM_INT;
                break;
            }
            
            case KEYW_ALL: {
                if (varToCount) { createInt(result, 0); break; }
                result.data.i = 0;
                result.mode = OM_ALL;
                result.subType = 0;
                break;
            }
            
            case KEYW_ALL_OTHER: {
                if (varToCount) { createInt(result, 0); break; }
                result.data.i = 0;
                result.mode = OM_ALL_OTHER;
                result.subType = 0;
                break;
            }
            
            case KEYW_NOTHING: {
                if (varToCount) { createInt(result, 0); break; }
                result.data.i = 0;
                result.mode = OM_NOTHING;
                result.subType = 0;
                break;
            }

            case KEYW_THIS: {
                if (varToCount) { createInt(result, 0); break; }
                result.data.i = 0;
                result.mode = OM_THIS;
                result.subType = scriptType;
                break;
            }
            
            case TOKEN_IDENTIFIER: {
                // function call/message send or variable
                t->skipToken();
                skippedToken = 1;

                // @TODO: no scope or subscript for now
                // @TODO: varToCount not supported for this portion entirely

                // member variable or function
                int searchFor = 0;
                Operand assumeThis;
                
                if (memberOf) {
                    if (OM_IS_LITERAL(memberOf->mode) || OM_IS_STR(memberOf->mode)) {
                        searchFor = SUB_ENTITY;
                    }
                    else if (OM_IS_OBJECT(memberOf->mode)) {
                        searchFor = memberOf->subType;
                    }
                    else if (OM_IS_ENTITY(memberOf->mode)) {
                        searchFor = SUB_ENTITY;
                    }
                    else compileAssert(0);
                }
                else {
                    // Basically "this." is presumed
                    searchFor = SUB_ENTITY;
                    assumeThis.data.i = 0;
                    assumeThis.mode = OM_THIS;
                    assumeThis.subType = scriptType;
                    memberOf = &assumeThis;
                }
                
                // Search for matching built-in member
                for (int pos = 0; memberTable[pos].objtype; ++pos) {
                    if ((memberTable[pos].objtype == searchFor) &&
                        (token == memberTable[pos].name)) {

                        // Ensure copy, not convert, for object pointer
                        if (!OM_IS_LITERAL(memberOf->mode) && OM_IS_CONVERT(memberOf->mode))
                            memberOf->mode = (OperMode)(memberOf->mode | OM_COPY);

                        // Use pop mode if appropriate
                        if (O_IS_TEMP(*memberOf)) {
                            compileAssert(memberOf->data.i == 0);
                            memberOf->mode = (OperMode)((memberOf->mode & ~OM_LOCATION) | OM_POP);
                        }

                        // Member found- assign or read?
                        if ((t->peekToken(type, token)) && (type == OPER_ASSIGN)) {
                            if (memberTable[pos].readonly)
                                t->outputError("Member '%s' is read-only (cannot assign a value to it)", token.c_str());
                                // RESOLUTION: assign anyways

                            if (warnAssign)
                                t->outputWarning("Assignment as a condition- did you mean '==' to compare? (surround assignment in extra parenthesis to suppress warning)");
                            t->skipToken();
                            // @TODO: Ensure handles all/all_other/string as memberOf
                            parseAssignment(*memberOf, memberTable[pos].membertype, NULL, memberTable[pos].index);

                            // @TODO: if we're usingValue, should be a OP_REFe instead of OP_SETe
                            // and skip OM_POP earlier; this irrelevant value is only valid if we aren't
                            compileAssert(!usingValue);
                            createInt(result, 0);
                            result.flags |= Operand::OF_IS_SIDEFX;

                            // If popped, adjust stack
                            if (OM_IS_POP(memberOf->mode))
                                --ws->stackDepth;
                        }
                        else {
                            Operand b, c;
                            createInt(b, memberTable[pos].index);

                            // Prep result now, as we use it's datatype
                            result = dataTypeToOperand(memberTable[pos].membertype);
                            result.mode = (OperMode)(result.mode | OM_STACK);
                            result.data.i = 0;

                            // Need a StackEntry datatype
                            createInt(c, result.mode & OM_BASETYPE);
                            // @TODO: Ensure handles all/all_other/string as memberOf
                            postCmdRaw(searchFor == SUB_ENTITY ? OP_GETe : OP_GETo, memberOf, &b, &c);

                            // If popped, it was replaced with newly created value;
                            // if not popped, we have a new value on the stack
                            if (!OM_IS_POP(memberOf->mode))
                                ++ws->stackDepth;
                        }

                        // (this tells caller that we handled the OPER_MEMBER
                        memberOf->mode = OM_NONE;
                        return result;
                    }
                }
                
                // Objects only have these specific members, so we're done
                if (searchFor != SUB_ENTITY) {
                    t->outputError("Unrecognized identifier '%s'- no object member by that name", token.c_str());
                    // RESOLUTION: discarded token
                    break;
                }
                
                // Special handing for this. (may be presumed)
                if (memberOf->mode == OM_THIS) {
                    // find matching function
                    Function f;
                    if (findFunction(token, f)) {
                        // (only vartocount if var isn't scoped)
                        if (varToCount) { createInt(result, (varToCount->scope <= 0) ? 1 : 0); break; }
                        result = parseFunction(f, token);
                        result.flags |= Operand::OF_IS_SIDEFX;
                        if ((usingValue) && (O_IS_VOID(result))) {
                            t->outputError("Function does not return a value (function returns 'void' and cannot be used within an expression)");
                            // RESOLUTION: proceed with int constant
                            result.flags &= ~Operand::OF_IS_VOID;
                        }
                        break;
                    }

                    // find matching variable
                    VariableMap::iterator vlist = vars->find(token.c_str());
                    if (vlist == vars->end()) {
                        // One last possibility- message send, only if we "presumed" this.
                        if (memberOf == &result) {
                            // @TODO: make this auto-conversion an option
                            string nextToken;
                            if ((t->peekToken(type, nextToken)) && (type == OPER_MEMBER)) {
                                if (varToCount) { createInt(result, 0); break; }
                                // Convert identifier to string and continue
                                result.data.i = 0;
                                result.ref.strLiteral = new string(token);
                                result.mode = OM_STR;
                                break;
                            }
                        }

                        t->outputError("Unrecognized identifier '%s'- no variable or function by that name", token.c_str());
                        // RESOLUTION: discarded token
                        break;
                    }

                    Variable& v = (*vlist).second.back();

                    // If assignment coming up, perform that now
                    if ((t->peekToken(type, token)) && (type == OPER_ASSIGN)) {
                        if (warnAssign)
                            t->outputWarning("Assignment as a condition- did you mean '==' to compare? (surround assignment in extra parenthesis to suppress warning)");
                        t->skipToken();
                        result = variableToOperand(v);
                        parseAssignment(result, v.datatype, varToCount);

                        // @TODO: optimize by getting operand from parseAssignment instead of
                        // turning variable into operand, when appropriate? (careful with arrays
                        // and hashes, they might need to become refs; careful with variants or
                        // type changes; careful with temporaries that would have been popped now;
                        // might also be affected by the optimization algorithm for a=a+1?)
                        result.flags |= Operand::OF_IS_SIDEFX;
                    }
                    else if (!varToCount) {
                        result = variableToOperand(v);
                    }
                    if (varToCount) { createInt(result, (&v == varToCount) ? 1 : 0); break; }
                }

                // Now it's either another entity, or an entity literal/string/etc.
                else {
                    // @TODO: Ensure handles all/all_other/string
                    // @TODO: Search for user-defined variable or function
                    compileAssert(0);
                }
                
                break;
            }

            case KEYW_SOURCE:
            case KEYW_DEFAULT: // (needs to basically follow identifier section, expecting a :: next)
            case OPER_PLUS:
            case OPER_MINUS:
            case OPER_B_NOT:
            case OPER_L_NOT: {
                // @TODO: L_NOT can be optimized if last operation on an operand before
                // an if-clause or sometimes before an expression consisting of other
                // logical/comparison operators
                // @TODO: (how to handle precedence for opers? just recurse to parseoperand; watch for void)
                if (varToCount) { createInt(result, 0); break; }
                t->outputError("Not currently supported: '%s'", token.c_str());
                break;
            }
            
            case TOKEN_OPEN_PAREN: {
                // Another expression within parenthesis
                t->skipToken();
                result = parseExpression(warnAssign > 0 ? warnAssign - 1 : warnAssign, usingValue, NULL, 100, varToCount);

                t->peekToken(type, token);
                if (type == TOKEN_CLOSE_PAREN) {
                    t->skipToken();
                }
                else {
                    t->outputError("Missing closing parenthesis");
                    // RESOLUTION: pretend paren was present
                }
                return result;
            }
            
            case TOKEN_ENDLINE: {
                t->outputError("Unexpected end-of-line (expected value or expression)", token.c_str());
                // RESOLUTION: token will get processed by higher routines
                break;
            }
            
            default: {
                t->outputError("Unexpected token '%s' (expected value or expression)", token.c_str());
                // RESOLUTION: token will get processed by higher routines
                break;
            }
        }
    }
    
    if (result.mode == 0) {
        // Error state
        result.data.i = 0;
    }
    else if (!skippedToken) {
        t->skipToken();
    }
    
    return result;
}

Compiler::Operand Compiler::parseOperation(int oper, const string& operToken, Operand& a, Operand& b) { start_func
    compileAssert(oper & TOKEN_OPERATOR);
    compileAssert(a.mode);
    compileAssert(!OM_IS_COPY(a.mode));
    compileAssert(!OM_IS_CONST(a.mode));
    compileAssert(!OM_IS_POP(a.mode));
    compileAssert(!OM_IS_POINTER(a.mode));
    compileAssert(b.mode);
    compileAssert(!OM_IS_COPY(b.mode));
    compileAssert(!OM_IS_CONST(b.mode));
    compileAssert(!OM_IS_POP(b.mode));
    compileAssert(!OM_IS_POINTER(b.mode));

    // Categorize operator- 4) string 3) math, 2) bit/mod, 1) logic, 0) equality
    int otype = 0;
    if ((oper == OPER_L_AND) || (oper == OPER_L_OR) || (oper == OPER_L_XOR))
        otype = 1;
    else if ((oper == OPER_LSHIFT) || (oper == OPER_RSHIFT) ||
             (oper == OPER_B_AND) || (oper == OPER_B_OR) ||
             (oper == OPER_B_XOR) || (oper == OPER_MOD))
        otype = 2;
    else if ((oper == OPER_PLUS) || (oper == OPER_MINUS) ||
             (oper == OPER_MULT) || (oper == OPER_DIV))
        otype = 3;
    else if (oper == OPER_CONCAT)
        otype = 4;
    
    // @TODO: convert var operands if unambiguous e.g. bit/mod, convert to int

    // General error: no hash or array or variant operands except for logical operators
    if (otype != 1) {
        if (OM_IS_ENTRY(a.mode)) {
            t->outputError("Left operand to '%s' cannot be a variant (convert to a non-variant first)", operToken.c_str());
            // RESOLUTION: continue with (hopefully) non-variant operand
            return b;
        }
        if (OM_IS_ENTRY(b.mode)) {
            t->outputError("Right operand to '%s' cannot be a variant (convert to a non-variant first)", operToken.c_str());
            // RESOLUTION: continue with non-variant operand
            return a;
        }
        if (OM_IS_ARRAY(a.mode)) {
            t->outputError("Left operand to '%s' cannot be an array (use [ and ] to select a value from the array)", operToken.c_str());
            // RESOLUTION: continue with (hopefully) non-array operand
            return b;
        }
        if (OM_IS_ARRAY(b.mode)) {
            t->outputError("Right operand to '%s' cannot be a array (use [ and ] to select a value from the array)", operToken.c_str());
            // RESOLUTION: continue with non-array operand
            return a;
        }
        if (OM_IS_HASH(a.mode)) {
            t->outputError("Left operand to '%s' cannot be a hash (use [ and ] to select a value from the hash)", operToken.c_str());
            // RESOLUTION: continue with (hopefully) non-hash operand
            return b;
        }
        if (OM_IS_HASH(b.mode)) {
            t->outputError("Right operand to '%s' cannot be a hash (use [ and ] to select a value from the hash)", operToken.c_str());
            // RESOLUTION: continue with non-hash operand
            return a;
        }
    }
    
    // General error: no string operands except for logical/comparison/string operators
    if ((otype > 1) && (otype < 4)) {
        if (OM_IS_STR(a.mode)) {
            t->outputError("Left operand to '%s' cannot be a string (convert to a number first)", operToken.c_str());
            // RESOLUTION: continue with (hopefully) non-string operand
            if (a.mode == OM_STR) delete a.ref.strLiteral;
            return b;
        }
        if (OM_IS_STR(b.mode)) {
            t->outputError("Right operand to '%s' cannot be a string (convert to a number first)", operToken.c_str());
            // RESOLUTION: continue with non-string operand
            if (b.mode == OM_STR) delete b.ref.strLiteral;
            return a;
        }
    }
    // General error: no object/"odd literal" except for logical/comparison operators
    if (otype > 1) {
        if (a.mode == OM_ALL || a.mode == OM_ALL_OTHER || a.mode == OM_NOTHING || a.mode == OM_THIS ||
            OM_IS_OBJECT(a.mode) || OM_IS_ENTITY(a.mode)) {
            t->outputError("Left operand to '%s' cannot be an entity or object", operToken.c_str());
            // RESOLUTION: continue with (hopefully) non-object operand
            return b;
        }
        if (b.mode == OM_ALL || b.mode == OM_ALL_OTHER || b.mode == OM_NOTHING || b.mode == OM_THIS ||
            OM_IS_OBJECT(b.mode) || OM_IS_ENTITY(b.mode)) {
            t->outputError("Right operand to '%s' cannot be an entity or object", operToken.c_str());
            // RESOLUTION: continue with non-object operand
            return a;
        }
    }
    else if (otype == 0) {
        // object-type operands are allowed here but must be compatible types
        if (a.mode == OM_ALL || a.mode == OM_ALL_OTHER || a.mode == OM_NOTHING || a.mode == OM_THIS ||
            b.mode == OM_ALL || b.mode == OM_ALL_OTHER || b.mode == OM_NOTHING || b.mode == OM_THIS ||
            OM_IS_OBJECT(a.mode) || OM_IS_ENTITY(a.mode) ||
            OM_IS_OBJECT(b.mode) || OM_IS_ENTITY(b.mode)) {
            // Literals are basically of type OM_ENTITY except for 'nothing'
            int aType = a.mode & OM_BASETYPE;
            int aSub = a.subType;
            int bType = b.mode & OM_BASETYPE;
            int bSub = b.subType;
            if (OM_IS_LITERAL(a.mode)) {
                if (a.mode == OM_ALL || a.mode == OM_ALL_OTHER) {
                    t->outputError("Cannot use multi-object literals ('all', 'all_other') in comparison", operToken.c_str());
                    // RESOLUTION: just continue with other operand for simplicity
                    return b;
                }
                if ((a.mode == OM_NOTHING) && (!OM_IS_LITERAL(b.mode))) {
                    aType = bType;
                    aSub = bSub;
                }
                else aType = OM_ENTITY;
            }
            if (OM_IS_LITERAL(b.mode)) {
                if (b.mode == OM_ALL || b.mode == OM_ALL_OTHER) {
                    t->outputError("Cannot use multi-object literals ('all', 'all_other') in comparison", operToken.c_str());
                    // RESOLUTION: just continue with other operand for simplicity
                    return a;
                }
                if ((b.mode == OM_NOTHING) && (!OM_IS_LITERAL(b.mode))) {
                    bType = aType;
                    bSub = aSub;
                }
                else bType = OM_ENTITY;
            }
            // Ensure both are same type
            if (aType != bType) {
                t->outputError("Cannot compare object to non-object or entity to non-entity using '%s' operator", operToken.c_str());
                // RESOLUTION: just continue with first operand for simplicity
                return a;
            }
            // Now ensure subtype matches exactly or is non-type vs type
            if ((aSub != bSub) && (aSub) && (bSub)) {
                t->outputError("Object or script types are not compatible for '%s' operator", operToken.c_str());
                // RESOLUTION: just continue with first operand for simplicity
                return a;
            }
        }
    }
    
    // Now we know the operands are valid modes- i/s/f, a/h/o for logicals only, l/s/g/ind/convert unverified
    // Perform standard conversions
    // One problem could occur- if b is a temporary and a converts to a temporary now,
    //    a will be on stack top, so a/b and operation needs reversing
    //    this won't ever occur with non-commutative operations because those
    //    will already have converted a to a temporary
    int watchForSwap = O_IS_TEMP(b) && !O_IS_TEMP(a);
    if (otype == 4) {
        // String- upgrade to string always
        if (!OM_IS_STR(a.mode))
            convertToStr(a);
        if (!OM_IS_STR(b.mode))
            convertToStr(b);
    }
    if (otype == 0) {
        // Equality- upgrade to string
        if (OM_IS_STR(a.mode) && !OM_IS_STR(b.mode))
            convertToStr(b);
        if (OM_IS_STR(b.mode) && !OM_IS_STR(a.mode))
            convertToStr(a);
    }
    if ((otype == 3) || (otype == 0)) {
        // Math or equality- upgrade to float
        if (OM_IS_FLOAT(a.mode) && !OM_IS_FLOAT(b.mode))
            convertToFloat(b);
        if (OM_IS_FLOAT(b.mode) && !OM_IS_FLOAT(a.mode))
            convertToFloat(a);
    }
    if (otype == 2) {
        // Bit/Mod- downgrade to integer
        if (!OM_IS_INT(a.mode))
            convertToInt(a);
        if (!OM_IS_INT(b.mode))
            convertToInt(b);
    }
    // This will be true if we need inverse operation now
    watchForSwap = watchForSwap && O_IS_TEMP(a);
    if (watchForSwap) swap(a, b);
    
    // Triggers common routines for most basic operators
    // Common routines do not support hash/array operands (logical operators)
    int doCommon = 0;
    int isCommutative = 1;
    int hasInverse = 0; // Can we swap operands using a different opcode? (equalities mostly)
    int pushesResult = !otype; // Equality operators push a new result
    Opcode forInt, forFloat, forStr, forObj;
    Opcode swapInt, swapFloat, swapStr, swapObj;
    
    switch (oper) {
        case OPER_PLUS: {
            // Const+const
            if (OM_IS_LITERAL(a.mode) && OM_IS_LITERAL(b.mode)) {
                // (both are the same due to earlier upgrades)
                if (OM_IS_FLOAT(a.mode))
                    a.data.f += b.data.f;
                else
                    a.data.i += b.data.i;
            }
            else {
                doCommon = 1;
                forInt = OP_ADD;
                forFloat = OP_ADDf;
            }
            break;
        }
        case OPER_MULT: {
            // Const*const
            if (OM_IS_LITERAL(a.mode) && OM_IS_LITERAL(b.mode)) {
                if (OM_IS_FLOAT(a.mode))
                    a.data.f *= b.data.f;
                else
                    a.data.i *= b.data.i;
            }
            else {
                doCommon = 1;
                forInt = OP_MULT;
                forFloat = OP_MULTf;
            }
            break;
        }
        case OPER_MINUS: {
            if (OM_IS_LITERAL(a.mode) && OM_IS_LITERAL(b.mode)) {
                if (OM_IS_FLOAT(a.mode))
                    a.data.f -= b.data.f;
                else
                    a.data.i -= b.data.i;
            }
            else {
                doCommon = 1;
                isCommutative = 0;
                forInt = OP_SUB;
                forFloat = OP_SUBf;
            }
            break;
        }
        case OPER_DIV: {
            if (OM_IS_LITERAL(a.mode) && OM_IS_LITERAL(b.mode)) {
                if (OM_IS_FLOAT(a.mode)) {
                    if (b.data.f == 0.0) {
                        t->outputError("Divide by zero");
                        // RESOLUTION: result is zero
                        a.data.f = 0.0;
                    }
                    else {
                        a.data.f /= b.data.f;
                    }
                }
                else {
                    if (b.data.i == 0) {
                        t->outputError("Divide by zero");
                        // RESOLUTION: result is zero
                        a.data.i = 0;
                    }
                    else {
                        a.data.i /= b.data.i;
                    }
                }
            }
            else {
                doCommon = 1;
                isCommutative = 0;
                forInt = OP_DIV;
                forFloat = OP_DIVf;
            }
            break;
        }
        case OPER_MOD: {
            if (OM_IS_LITERAL(a.mode) && OM_IS_LITERAL(b.mode)) {
                if (b.data.i == 0) {
                    t->outputError("Divide by zero");
                    // RESOLUTION: result is zero
                    a.data.i = 0;
                }
                else {
                    a.data.i %= b.data.i;
                }
            }
            else {
                doCommon = 1;
                isCommutative = 0;
                forInt = OP_MOD;
            }
            break;
        }
        case OPER_LSHIFT: {
            if (OM_IS_LITERAL(a.mode) && OM_IS_LITERAL(b.mode)) {
                a.data.i <<= b.data.i;
            }
            else {
                doCommon = 1;
                isCommutative = 0;
                forInt = OP_SHIFTL;
            }
            break;
        }
        case OPER_RSHIFT: {
            if (OM_IS_LITERAL(a.mode) && OM_IS_LITERAL(b.mode)) {
                a.data.i >>= b.data.i;
            }
            else {
                doCommon = 1;
                isCommutative = 0;
                forInt = OP_SHIFTR;
            }
            break;
        }
        case OPER_B_AND: {
            if (OM_IS_LITERAL(a.mode) && OM_IS_LITERAL(b.mode)) {
                a.data.i &= b.data.i;
            }
            else {
                doCommon = 1;
                forInt = OP_AND;
            }
            break;
        }
        case OPER_B_OR: {
            if (OM_IS_LITERAL(a.mode) && OM_IS_LITERAL(b.mode)) {
                a.data.i |= b.data.i;
            }
            else {
                doCommon = 1;
                forInt = OP_OR;
            }
            break;
        }
        case OPER_B_XOR: {
            if (OM_IS_LITERAL(a.mode) && OM_IS_LITERAL(b.mode)) {
                a.data.i ^= b.data.i;
            }
            else {
                doCommon = 1;
                forInt = OP_XOR;
            }
            break;
        }
        case OPER_CONCAT: {
            if (OM_IS_LITERAL(a.mode) && OM_IS_LITERAL(b.mode)) {
                compileAssert(a.ref.strLiteral);
                compileAssert(b.ref.strLiteral);
                *a.ref.strLiteral += *b.ref.strLiteral;
            }
            else {
                doCommon = 1;
                isCommutative = 0;
                forStr = OP_CONCAT;
            }
            break;
        }
        case OPER_EQ: {
            if (OM_IS_LITERAL(a.mode) && OM_IS_LITERAL(b.mode)) {
                if (OM_IS_STR(a.mode)) {
                    compileAssert(a.ref.strLiteral);
                    compileAssert(b.ref.strLiteral);
                    a.data.i = (myStricmp(a.ref.strLiteral->c_str(), b.ref.strLiteral->c_str()) == 0);
                    delete a.ref.strLiteral;
                    a.ref.strLiteral = NULL;
                }
                else if (OM_IS_FLOAT(a.mode))
                    a.data.i = (a.data.f == b.data.f);
                // Only going to be a combination of this/empty
                else if ((a.mode == OM_THIS) || (a.mode == OM_NOTHING))
                    a.data.i = (a.mode == b.mode);
                else
                    a.data.i = (a.data.i == b.data.i);
                compileAssert(a.flags == 0);
                a.mode = OM_INT;
            }
            else {
                doCommon = 1;
                forInt = OP_EQ;
                forFloat = OP_EQf;
                forStr = OP_EQs;
                forObj = OP_EQo;
            }
            break;
        }
        case OPER_LE: {
            if (OM_IS_LITERAL(a.mode) && OM_IS_LITERAL(b.mode)) {
                if (OM_IS_STR(a.mode)) {
                    compileAssert(a.ref.strLiteral);
                    compileAssert(b.ref.strLiteral);
                    a.data.i = (myStricmp(a.ref.strLiteral->c_str(), b.ref.strLiteral->c_str()) <= 0);
                    delete a.ref.strLiteral;
                    a.ref.strLiteral = NULL;
                }
                else if (OM_IS_FLOAT(a.mode))
                    a.data.i = (a.data.f <= b.data.f);
                // Only going to be a combination of this/empty
                else if ((a.mode == OM_THIS) || (a.mode == OM_NOTHING))
                    a.data.i = (a.mode == b.mode);
                else
                    a.data.i = (a.data.i <= b.data.i);
                compileAssert(a.flags == 0);
                a.mode = OM_INT;
            }
            else {
                doCommon = 1;
                isCommutative = 0;
                hasInverse = 1;
                forInt = OP_LE;
                forFloat = OP_LEf;
                forStr = OP_LEs;
                forObj = OP_EQo;
                swapInt = OP_GE;
                swapFloat = OP_GEf;
                swapStr = OP_GEs;
                swapObj = OP_EQo;
            }
            break;
        }
        case OPER_GT: {
            if (OM_IS_LITERAL(a.mode) && OM_IS_LITERAL(b.mode)) {
                if (OM_IS_STR(a.mode)) {
                    compileAssert(a.ref.strLiteral);
                    compileAssert(b.ref.strLiteral);
                    a.data.i = (myStricmp(a.ref.strLiteral->c_str(), b.ref.strLiteral->c_str()) > 0);
                    delete a.ref.strLiteral;
                    a.ref.strLiteral = NULL;
                }
                else if (OM_IS_FLOAT(a.mode))
                    a.data.i = (a.data.f > b.data.f);
                // Only going to be a combination of this/empty
                else if ((a.mode == OM_THIS) || (a.mode == OM_NOTHING))
                    a.data.i = (a.mode != b.mode);
                else
                    a.data.i = (a.data.i > b.data.i);
                compileAssert(a.flags == 0);
                a.mode = OM_INT;
            }
            else {
                doCommon = 1;
                isCommutative = 0;
                hasInverse = 1;
                forInt = OP_GT;
                forFloat = OP_GTf;
                forStr = OP_GTs;
                forObj = OP_NEo;
                swapInt = OP_LT;
                swapFloat = OP_LTf;
                swapStr = OP_LTs;
                swapObj = OP_NEo;
            }
            break;
        }
        case OPER_GE: {
            if (OM_IS_LITERAL(a.mode) && OM_IS_LITERAL(b.mode)) {
                if (OM_IS_STR(a.mode)) {
                    compileAssert(a.ref.strLiteral);
                    compileAssert(b.ref.strLiteral);
                    a.data.i = (myStricmp(a.ref.strLiteral->c_str(), b.ref.strLiteral->c_str()) >= 0);
                    delete a.ref.strLiteral;
                    a.ref.strLiteral = NULL;
                }
                else if (OM_IS_FLOAT(a.mode))
                    a.data.i = (a.data.f >= b.data.f);
                // Only going to be a combination of this/empty
                else if ((a.mode == OM_THIS) || (a.mode == OM_NOTHING))
                    a.data.i = (a.mode == b.mode);
                else
                    a.data.i = (a.data.i >= b.data.i);
                compileAssert(a.flags == 0);
                a.mode = OM_INT;
            }
            else {
                doCommon = 1;
                isCommutative = 0;
                hasInverse = 1;
                forInt = OP_GE;
                forFloat = OP_GEf;
                forStr = OP_GEs;
                forObj = OP_EQo;
                swapInt = OP_LE;
                swapFloat = OP_LEf;
                swapStr = OP_LEs;
                swapObj = OP_EQo;
            }
            break;
        }
        case OPER_LT: {
            if (OM_IS_LITERAL(a.mode) && OM_IS_LITERAL(b.mode)) {
                if (OM_IS_STR(a.mode)) {
                    compileAssert(a.ref.strLiteral);
                    compileAssert(b.ref.strLiteral);
                    a.data.i = (myStricmp(a.ref.strLiteral->c_str(), b.ref.strLiteral->c_str()) < 0);
                    delete a.ref.strLiteral;
                    a.ref.strLiteral = NULL;
                }
                else if (OM_IS_FLOAT(a.mode))
                    a.data.i = (a.data.f < b.data.f);
                // Only going to be a combination of this/empty
                else if ((a.mode == OM_THIS) || (a.mode == OM_NOTHING))
                    a.data.i = (a.mode != b.mode);
                else
                    a.data.i = (a.data.i < b.data.i);
                compileAssert(a.flags == 0);
                a.mode = OM_INT;
            }
            else {
                doCommon = 1;
                isCommutative = 0;
                hasInverse = 1;
                forInt = OP_LT;
                forFloat = OP_LTf;
                forStr = OP_LTs;
                forObj = OP_NEo;
                swapInt = OP_GT;
                swapFloat = OP_GTf;
                swapStr = OP_GTs;
                swapObj = OP_NEo;
            }
            break;
        }
        case OPER_NE: {
            if (OM_IS_LITERAL(a.mode) && OM_IS_LITERAL(b.mode)) {
                if (OM_IS_STR(a.mode)) {
                    compileAssert(a.ref.strLiteral);
                    compileAssert(b.ref.strLiteral);
                    a.data.i = (myStricmp(a.ref.strLiteral->c_str(), b.ref.strLiteral->c_str()) != 0);
                    delete a.ref.strLiteral;
                    a.ref.strLiteral = NULL;
                }
                else if (OM_IS_FLOAT(a.mode))
                    a.data.i = (a.data.f != b.data.f);
                // Only going to be a combination of this/empty
                else if ((a.mode == OM_THIS) || (a.mode == OM_NOTHING))
                    a.data.i = (a.mode != b.mode);
                else
                    a.data.i = (a.data.i != b.data.i);
                compileAssert(a.flags == 0);
                a.mode = OM_INT;
            }
            else {
                doCommon = 1;
                forInt = OP_NE;
                forFloat = OP_NEf;
                forStr = OP_NEs;
                forObj = OP_NEo;
            }
            break;
        }
        
        case OPER_L_AND:
        case OPER_L_XOR:
        case OPER_L_OR: {
            // @TODO: (for now will just return first operand)
            // commutative, so no need to check watchForSwap
            // Also note that operands may have disparate types, be 'this'/'nothing'/'all'/'all_other',
            // or even be variants!
            // @TODO: short-circuiting if enabled
            t->outputError("Not currently supported: '%s'", operToken.c_str());
            break;
        }
        
        default: {
            // (should never get past other operations)
            compileAssert(0);
            break;
        }
    }
    
    // Common operator routine
    if (doCommon) {
        // Push-result commands must be swappable
        if (pushesResult) compileAssert(isCommutative || hasInverse);
        
        // Presumes temporaries are on top of stack
        compileAssert(!O_IS_TEMP(a) || (a.data.i == 0));
        compileAssert(!O_IS_TEMP(b) || (b.data.i == 0));
        
        // If both items are temporaries, "move" first item down to it's true position
        if (O_IS_TEMP(a) && O_IS_TEMP(b)) ++a.data.i;
        
        // Different handling based on whether we overwrite first operand
        if (pushesResult) {
            // Just confirm first item isn't the literal; any other combination is valid
            if (OM_IS_LITERAL(a.mode)) {
                // Literal goes in second position
                swap(a, b);
                // must use the inverse operation if exists
                if (hasInverse) watchForSwap = !watchForSwap;
            }
        }
        else {
            // (*for non-commutative operations, first should already be temporary*)
            if (!isCommutative) compileAssert(O_IS_TEMP(a));

            // One item MUST be a temporary (ideally, first item)
            if (!O_IS_TEMP(a) && !O_IS_TEMP(b)) {
                // Create temporary via PUSH- no reference allowed
                convertToTemp(a);
                // This moves other stack entry down if it's a stack var or temporary
                if (OM_IS_STACK(b.mode)) {
                    ++b.data.i;
                }
            }
            
            // Possibilities now: var + temp, temp + temp, temp + literal
            // First item must be a temporary
            // (*never occurs if this is a commutative operation or both are temps*)
            if (!O_IS_TEMP(a)) swap(a, b);
            
            // First item should also be a copy (we know it's a temporary)
            // HOWEVER as we can never have arrays, hashes, variants here it's moot
            compileAssert(!(OM_IS_ARRAY(a.mode) || OM_IS_HASH(a.mode) || OM_IS_ENTRY(a.mode)));
        }

        // Handle COPYness now, before doing POP
        if (pushesResult && !OM_IS_LITERAL(a.mode) && OM_IS_CONVERT(a.mode))
            a.mode = (OperMode)(a.mode | OM_COPY);
        if (!OM_IS_LITERAL(b.mode) && OM_IS_CONVERT(b.mode))
            b.mode = (OperMode)(b.mode | OM_COPY);

        // If second item is a temporary, convert to a POP
        // (*ok since this isn't hash/array*)
        if (O_IS_TEMP(b)) {
            compileAssert(b.data.i == 0);
            b.mode = (OperMode)((b.mode & ~OM_LOCATION) | OM_POP);
        }
        
        // If first item is a temporary and we push result, convert that to a POP too
        if (pushesResult && O_IS_TEMP(a)) {
            a.mode = (OperMode)((a.mode & ~OM_LOCATION) | OM_POP);
            // If was further down on the stack...
            if (a.data.i == 1) {
                // ...b is also a temporary that's been converted
                compileAssert(OM_IS_POP(b.mode));
                compileAssert(b.data.i == 0);
                // a and b will get popped in reverse order, so we
                // must use the inverse operation if exists
                if (hasInverse) watchForSwap = !watchForSwap;
            }
        }
        
        // Need to swap to inverse operation?
        if (watchForSwap) {
            if (hasInverse) {
                forStr = swapStr;
                forFloat = swapFloat;
                forInt = swapInt;
                forObj = swapObj;
            }
            else compileAssert(isCommutative);
        }
        
        // Post command (we know a/b aren't variants)

        // Define strings
        if (a.mode == OM_STR) defineString(a);
        if (b.mode == OM_STR) defineString(b);

        if (OM_IS_STR(a.mode)) {
            compileAssert(OM_IS_STR(b.mode));
            // Handle CONSTness
            if (pushesResult) a.mode = (OperMode)(OM_STR_CONST | (a.mode & ~OM_BASETYPE));
            b.mode = (OperMode)(OM_STR_CONST | (b.mode & ~OM_BASETYPE));
            postCmdRaw(forStr, &a, &b);
        }
        else if (OM_IS_INT(a.mode)) {
            compileAssert(OM_IS_INT(b.mode));
            postCmdRaw(forInt, &a, &b);
        }
        else if (OM_IS_FLOAT(a.mode)) {
            compileAssert(OM_IS_FLOAT(b.mode));
            postCmdRaw(forFloat, &a, &b);
        }
        else {
            compileAssert(OM_IS_ENTITY(a.mode) || OM_IS_OBJECT(a.mode) || a.mode == OM_THIS || a.mode == OM_NOTHING);
            compileAssert(OM_IS_ENTITY(b.mode) || OM_IS_OBJECT(b.mode) || b.mode == OM_THIS || b.mode == OM_NOTHING);
            postCmdRaw(forObj, &a, &b);
        }
        
        // (move item back up if popped; also adjust stack depth)
        if (OM_IS_POP(b.mode)) {
            --ws->stackDepth;
            if (O_IS_TEMP(a)) --a.data.i;
        }
        if (OM_IS_POP(a.mode))
            --ws->stackDepth;
        
        // New result? (always an int- logical ops)
        if (pushesResult) {
            // a.ref.strLiteral is already NULL if there was anything (defineString() above)
            ++ws->stackDepth;
            createInt(a, 0);
            a.mode = (OperMode)(OM_INT | OM_STACK | OM_NO_CONVERT);
        }
        
        else {
            // Otherwise, mark result with new status- we know it's the right type now
            a.mode = (OperMode)((a.mode & ~OM_FLAGS) | OM_NO_CONVERT);
            // (would set OF_IS_COPY except we know it's not an array or hash in this code)
        }
    }
    else {
        destroyOperand(b);
    }

    // None of these operations have useful side effects. (@TODO: this won't be true for OPER_MEMBER)
    a.flags &= ~Operand::OF_IS_SIDEFX;
    return a;
}

const Variable& Compiler::defineVariable(DataType type, const string& name, int isParam) { start_func
    // Possibilities-
    //      global var
    //      local var in outer scope (local to entire script)
    //      local var inner scope (local to block)
    //      local var function parameter
    //      global const
    //      local const outer scope
    //      local const inner scope
    
    Variable var;
    
    // Global / local / scoped
    var.datatype = type;
    var.datatype.flags &= ~(DATA_LOCAL | DATA_GLOBAL | DATA_CONST);
    if (type.flags & DATA_GLOBAL) {
        var.scope = -1;
    }
    else {
        var.scope = scope;
    }
    
    // Const
    if (type.flags & DATA_CONST) {
        compileAssert(!isParam);
        
        // @TODO: assign actual value once expression parsing is complete
        if (type.baseType == DATA_INT)
            var.val.constI = 0;
        else if (type.baseType == DATA_FLOAT)
            var.val.constF = 0.0;
        else {
            char* newCC = new char[1];
            newCC[0] = 0;
            var.val.constS = newCC;
        }
        // @TODO: Global consts need go into links to verify against each other during linking
    }
    // Non-globals
    else if (var.scope >= 0) {
        // inner scope- next stack item
        if (var.scope > 0) {
            // adjust for preexisting stack depth
            // and adjust current stack depth
            var.val.offs = -(++ws->stackDepth);
        }

        else {
            compileAssert(!isParam);
        
            // Take up next init spot
            var.val.offs = localInit.stackDepth++;
            // Code only goes to init for outer scope
            pushWorkspace(&localInit);
        }
        
        if (!isParam) {
            // @TODO: original assignment, if not outer scope, should be done instead
            // even outer scope assignments can be done if at top of script and simple
            generateBlank(type);
        }

        if (var.scope == 0) popWorkspace();
    }
    // Global will get added to link table whenever used
    else {
        compileAssert(!isParam);

        var.val.offs = 0;
    }
    
    // Matches a function?
    FunctionMap::iterator flist = func->find(name.c_str());
    if (flist != func->end()) {
        t->outputError("Function '%s' already exists in this script (variables and functions cannot share names)", name.c_str());
        // RESOLUTION: define variable anyways
    }

    // Matches a built-in?
    for (int pos = 0; memberTable[pos].objtype; ++pos) {
        if ((memberTable[pos].objtype == SUB_ENTITY) &&
            (name == memberTable[pos].name)) {
            t->outputError("'%s' is a built-in entity member (variables cannot override built-in members)", name.c_str());
            // RESOLUTION: define variable anyways
        }
    }

    // @TODO: error if already defined at this scope
    list<Variable>& found = addVariableMap(vars, name, var.name);
    // Globals go to the "back" of the line (overridden by any existing locals)
    if (var.scope < 0) {
        found.push_front(var);
        return found.front();
    }
    // Everything else overrides any prior variable of the same name
    found.push_back(var);
    return found.back();
}

void Compiler::generateBlank(DataType& type) { start_func
    if (type.flags & (DATA_ARRAY | DATA_HASH))
        postCmdII((type.flags & DATA_ARRAY) ? OP_CREATEa : OP_CREATEh,
                  type.baseType == DATA_VAR ? DATA_INT : type.baseType,
                  (type.baseType == DATA_ENTITY || type.baseType == DATA_OBJECT) ? type.subType : 0);
    else if (type.baseType == DATA_FLOAT)
        postCmdF(OP_PUSHf, 0.0);
    else if (type.baseType == DATA_STR)
        postCmdS(OP_PUSHs, defineString(blankString));
    else if (type.baseType == DATA_ENTITY)
        postCmd(OP_CREATEe);
    else if (type.baseType == DATA_OBJECT)
        postCmd(OP_CREATEo);
    else {
        compileAssert((type.baseType == DATA_VAR) || (type.baseType == DATA_INT));
        // (default for variants is integer 0)
        postCmdI(OP_PUSH, 0);
    }
}

void Compiler::defineString(Operand& o) { start_func
    compileAssert(o.mode == OM_STR);
    
    if (o.ref.strLiteral) {
        o.data.i = defineString(*o.ref.strLiteral);
        delete o.ref.strLiteral;
        o.ref.strLiteral = NULL;
    }
}

int Compiler::defineString(const string& str) { start_func
    // Existing element?
    StringMap::iterator found = stringMap.find(str.c_str());
    if (found != stringMap.end()) {
        // Use existing string
        return (*found).second;
    }
    
    // New string will go at end
    int offset = strings.code.size();
    
    // Translating a string to 32-bit ints in this manner (and untranslating
    // by using a char* ptr later) should be platform-neutral.
    
    // Turn into an array of 32-bit ints; includes a byte for terminating NUL
    int length = (str.size() + sizeof(Uint32)) / sizeof(Uint32);
    Uint32* uiCopy = new Uint32[length];
    memcpy(uiCopy, str.c_str(), str.size() + 1);
    
    // Push onto strings
    strings.code.reserve(offset + length);
    for (int pos = 0; pos < length; ++pos)
        strings.code.push_back(uiCopy[pos]);
    
    // Remember offset for reuse
    stringMap[newCpCopy(str.c_str())] = offset;
    
    delete[] uiCopy;
    return offset;
}

void Compiler::prepLinker(const Variable* var, int toData) { start_func
    compileAssert(var);
    compileAssert(var->scope < 0);
    
    LinkEntry prepLink;
    prepLink.type = toData ? LinkEntry::LINK_GLOBAL : LinkEntry::LINK_GLOBALVAR;
    prepLink.offset = ws->code.size();
    prepLink.scope = blankString;
    prepLink.name = var->name;
    prepLink.wart = dataTypeToWart(var->datatype);
    ws->links.push_back(prepLink);
}

void Compiler::prepLinker(const string& funcName) { start_func
    LinkEntry prepLink;
    prepLink.type = LinkEntry::LINK_FUNCTION;
    prepLink.offset = ws->code.size() + 1;
    prepLink.scope = blankString;
    prepLink.name = funcName;
    prepLink.wart = blankString;
    ws->links.push_back(prepLink);
}

void Compiler::prepLinker(int isContinue, const string& loopName) { start_func
    LinkEntry prepLink;
    prepLink.type = isContinue ? LinkEntry::LINK_LOOP_CONTINUE : LinkEntry::LINK_LOOP_END;
    prepLink.offset = ws->code.size() + 1;
    prepLink.scope = blankString;
    prepLink.name = loopName;
    prepLink.wart = blankString;
    ws->links.push_back(prepLink);
}

void Compiler::prepLinker() { start_func
    LinkEntry prepLink;
    prepLink.type = LinkEntry::LINK_STRING;
    prepLink.offset = ws->code.size();
    prepLink.scope = blankString;
    prepLink.name = blankString;
    prepLink.wart = blankString;
    ws->links.push_back(prepLink);
}

int Compiler::postCmdRaw(Opcode opc, Operand* o1, Operand* o2, Operand* o3) { start_func
    Operand* opers[3] = { o1, o2, o3 };
    Uint8 oms[3] = { o1 ? o1->mode : OM_NONE,
                     o2 ? o2->mode : OM_NONE,
                     o3 ? o3->mode : OM_NONE };

    // Must be OM_STR_CONST by now
    compileAssert(oms[0] != OM_STR);
    compileAssert(oms[1] != OM_STR);
    compileAssert(oms[2] != OM_STR);

#ifdef COMPILEASSERT
    // Checks for valid opcode/mode combinations
    checkOpcode(opc, oms[0], oms[1], oms[2]);
#endif

    // Create opcode/mode combination
    Uint32 opcFinal = (((((oms[2] << 8) | oms[1]) << 8) | oms[0]) << 8) | opc;

    // Always store opcode
    ws->code.push_back(opcFinal);
    int used = 1;
    
    // Operands
    for (int pos = 0; pos < 3; ++pos) {
        // (skip empty operands)
        if (oms[pos]) {
            compileAssert(opers[pos]);
            
            // @TODO: array/hash conversion/copy should add type+subtype! (see convertToArray)
            
            // Adding subtype- any conversion or copy to entity or object (verify not literal)
            // OR all/all_other literals
            if ((((oms[pos] & OM_BASETYPE) == OM_ENTITY || (oms[pos] & OM_BASETYPE) == OM_OBJECT) &&
                  (oms[pos] & OM_FLAGS) != OM_NO_CONVERT && (oms[pos] & OM_FLAGS) != OM_INDIRECT &&
                  (oms[pos] & OM_BASETYPE) != oms[pos])
                || oms[pos] == OM_ALL || oms[pos] == OM_ALL_OTHER) {
                ws->code.push_back(opers[pos]->subType);
                ++used;
            }
            
            // Special literals, pop- nothing more
            if (((oms[pos] >= OM_ALL) && (oms[pos] < OM_POINTER)) || OM_IS_POP(oms[pos]))
                continue;
            
            // Linking globals
            if (OM_IS_GLOBAL(oms[pos])) {
                prepLinker(opers[pos]->ref.global);
                assert(opers[pos]->data.p == NULL);
            }
            
            // Linking static strings
            // After linking, this will be-
            //   String offset within strings
            //  +Offset of string section
            //  -Offset of operand itself
            if (oms[pos] == OM_STR_CONST) {
                prepLinker();
                ws->code.push_back(opers[pos]->data.i);
                ++used;
            }
            
            // Pushing float
            else if (oms[pos] == OM_FLOAT) {
                Uint32 buffer[bcFloatSize];
                memset(buffer, 0, sizeof(buffer[bcFloatSize]));
                *(BCfloat*)(&buffer) = opers[pos]->data.f;
                
                for (int pos = 0; pos < bcFloatSize; ++pos)
                    ws->code.push_back(buffer[pos]);
                used += bcFloatSize;
            }
            
            // Pushing pointer
            else if (OM_IS_POINTER(oms[pos]) || OM_IS_GLOBAL(oms[pos])) {
                Uint32 buffer[vPtrSize];
                memset(buffer, 0, sizeof(buffer[vPtrSize]));
                *(void**)(&buffer) = opers[pos]->data.p;
                
                for (int pos = 0; pos < bcFloatSize; ++pos)
                    ws->code.push_back(buffer[pos]);
                used += vPtrSize;
            }

/* @TODO: may optimize things to have stack refs be + 1
            // Pushing stack
            else if (OM_IS_STACK(oms[pos])) {
                ws->code.push_back(opers[pos]->data.i + 1);
                ++used;
            }
*/
            
            // Pushing int
            else {
                ws->code.push_back(opers[pos]->data.i);
                ++used;
            }
        }
    }

    // Display?
    if (debugLevel() & DEBUG_BYTECODE) {
        int start = ws->code.size() - used;
        Uint32* temp = new Uint32[used];
        for (int copy = 0; copy < used; ++copy) {
            temp[copy] = ws->code[start + copy];
        }
        
        string line;
        debugBytecode(line, temp);
        debugWrite("%s", line.c_str());
        
        delete[] temp;
    }
    
    return used;
}

void Compiler::postCmd(Opcode opc) { start_func
    postCmdRaw(opc);
}
void Compiler::postCmdI(Opcode opc, Sint32 int1) { start_func
    Operand o;
    createInt(o, int1);
    postCmdRaw(opc, &o);
}
void Compiler::postCmdII(Opcode opc, Sint32 int1, Sint32 int2) { start_func
    Operand o1;
    Operand o2;
    createInt(o1, int1);
    createInt(o2, int2);
    postCmdRaw(opc, &o1, &o2);
}
void Compiler::postCmdF(Opcode opc, BCfloat float1) { start_func
    Operand o;
    createFloat(o, float1);
    postCmdRaw(opc, &o);
}
void Compiler::postCmdS(Opcode opc, Sint32 str1) { start_func
    Operand o;
    createInt(o, str1);
    o.mode = OM_STR_CONST;
    postCmdRaw(opc, &o);
}

void Compiler::postCmdPush(Operand& oper, int refOK) { start_func
    compileAssert(oper.mode);
    compileAssert(!OM_IS_COPY(oper.mode));
    compileAssert(!OM_IS_CONST(oper.mode));
    compileAssert(!OM_IS_POP(oper.mode));
    compileAssert(!OM_IS_POINTER(oper.mode));

    Opcode opc = OP_PUSHo;
    
    // TODO: ensure pushv does ALL ref/deref needed, conversions, and copying new array/hash
    if (OM_IS_ENTRY(oper.mode)) opc = OP_PUSHv;
    else if (OM_IS_ARRAY(oper.mode)) opc = OP_PUSHa;
    else if (OM_IS_HASH(oper.mode)) opc = OP_PUSHh;
    else if (OM_IS_INT(oper.mode)) opc = OP_PUSH;
    else if (OM_IS_FLOAT(oper.mode)) opc = OP_PUSHf;
    else if (OM_IS_STR(oper.mode)) opc = OP_PUSHs;

    if (!OM_IS_LITERAL(oper.mode)) {
        // Source already has conversion flag set if necessary, but it needs to
        // be a 'copy' flag
        if (OM_IS_CONVERT(oper.mode))
            oper.mode = (OperMode)(oper.mode | OM_COPY);
            
        // Force a copy if non-unique array/hash and no refs allowed
        // (known flag precludes this from being an indirect, too)
        else if (!refOK && OM_IS_KNOWN(oper.mode) && (OM_IS_ARRAY(oper.mode) || OM_IS_HASH(oper.mode)) && !O_IS_COPY(oper) && (opc != OP_PUSHv))
            oper.mode = (OperMode)((oper.mode & ~OM_FLAGS) | OM_COPY);
    }
        
    // Source also needs to be const
    if (OM_IS_STR(oper.mode))
        oper.mode = (OperMode)(OM_STR_CONST | (oper.mode & ~OM_BASETYPE));

    postCmdRaw(opc, &oper);
}

int Compiler::postCmdIf(int ifTrue, Sint32 offset, Operand& o, int& alwaysJumps, int& neverJumps) { start_func
    compileAssert(o.mode);
    compileAssert(!OM_IS_COPY(o.mode));
    compileAssert(!OM_IS_CONST(o.mode));
    compileAssert(!OM_IS_POP(o.mode));
    compileAssert(!OM_IS_POINTER(o.mode));
    
    int curPos = ws->code.size();
    int result = -1;
    int alwaysTrue = 0;
    
    // If item is literal, always true or false
    if (OM_IS_LITERAL(o.mode)) {
        if (OM_IS_INT(o.mode)) alwaysTrue = o.data.i;
        else if (OM_IS_FLOAT(o.mode)) alwaysTrue = (o.data.f != 0.0);
        else if (OM_IS_STR(o.mode)) {
            compileAssert(o.ref.strLiteral);
            alwaysTrue = (*o.ref.strLiteral != blankString);
        }
        // 'nothing' is always false
        else if (o.mode == OM_NOTHING) alwaysTrue = 0;
        // All other 'literals' are true
        else alwaysTrue = 1;

        if (alwaysTrue) {
            t->outputWarning("Condition is always true");
        }
        // If always false...
        else {
            t->outputWarning("Condition is always false");
            // Reverse condition
            alwaysTrue = 1;
            ifTrue = !ifTrue;
        }
    }
    
    alwaysJumps = 0;
    neverJumps = 0;
    
    if (alwaysTrue) {
        destroyOperand(o);
        if (ifTrue) {
            alwaysJumps = 1;
            // 2 = adjusted by size of opcode we're about to add
            if (offset < 0) offset -= ws->code.size() - curPos + 2;
            result = ws->code.size() + 1; // 1 = position of offset operand
            postCmdI(OP_JUMP, offset);
        }
        else {
            neverJumps = 1;
        }
        return result;
    }

    // Default to object, as that covers a lot of types
    Opcode opc = ifTrue ? OP_IFTRUEo : OP_IFFALSEo;
    if (OM_IS_STR(o.mode)) opc = ifTrue ? OP_IFTRUEs : OP_IFFALSEs;
    else if (OM_IS_FLOAT(o.mode)) opc = ifTrue ? OP_IFTRUEf : OP_IFFALSEf;
    else if (OM_IS_INT(o.mode)) opc = ifTrue ? OP_IFTRUE : OP_IFFALSE;
    else if (OM_IS_ARRAY(o.mode)) opc = ifTrue ? OP_IFTRUEa : OP_IFFALSEa;
    else if (OM_IS_HASH(o.mode)) opc = ifTrue ? OP_IFTRUEh : OP_IFFALSEh;
    else if (OM_IS_ENTRY(o.mode)) opc = ifTrue ? OP_IFTRUEv : OP_IFFALSEv;
    
    // Source already has conversion flag set if necessary, but it needs to
    // be a 'copy' flag; we already know it's not a literal.
    if (OM_IS_CONVERT(o.mode))
        o.mode = (OperMode)(o.mode | OM_COPY);
        
    // Source also needs to be const; define string literals
    if (OM_IS_STR(o.mode))
        o.mode = (OperMode)(OM_STR_CONST | (o.mode & ~OM_BASETYPE));

    // Finally, source could be a pop; temporaries should never be variants
    if (O_IS_TEMP(o)) {
        compileAssert(!OM_IS_ENTRY(o.mode));
        compileAssert(o.data.i == 0);
        o.mode = (OperMode)((o.mode & ~OM_LOCATION) | OM_POP);
    }
    
    // Post command
    if (offset < 0) offset -= ws->code.size() - curPos;

    Operand jump;
    createInt(jump, offset);
    int dist = postCmdRaw(opc, &o, &jump);
    
    // We KNOW that the last operand, the jump, is 1 byte, so we can
    // deduce the position of the jump
    result = ws->code.size() - 1;
    
    // We need to adjust the jump, if it was negative, to cover the
    // size of the op.
    if (offset < 0) ws->code[result] -= dist;

    // (adjust stack depth if popped)
    if (OM_IS_POP(o.mode))
        --ws->stackDepth;
        
    return result;
}
